march-cli 0.1.9 → 0.1.11
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/editing/lsp-report.mjs +69 -0
- package/src/agent/file-edit-tool.mjs +10 -24
- package/src/agent/model-payload-dumper.mjs +11 -4
- package/src/agent/runner/runner-utils.mjs +18 -0
- package/src/agent/runner.mjs +26 -27
- package/src/agent/runtime/runner-runtime-host.mjs +2 -0
- package/src/agent/turn/turn-logging.mjs +30 -0
- package/src/agent/turn/turn-runner.mjs +40 -0
- package/src/cli/commands/status-command.mjs +4 -16
- package/src/cli/permissions.mjs +1 -1
- package/src/cli/shell/shell-drawer.mjs +1 -1
- package/src/cli/startup/runtime-close.mjs +23 -0
- package/src/cli/tui/output/visible-lines.mjs +8 -0
- package/src/cli/tui/output-buffer.mjs +30 -21
- package/src/cli/tui/render/stream-delta-buffer.mjs +46 -0
- package/src/cli/tui/selection-screen.mjs +12 -4
- package/src/cli/tui/tool-rendering.mjs +1 -1
- package/src/cli/ui.mjs +16 -17
- package/src/config/loader.mjs +28 -1
- package/src/context/system-core/base.md +1 -1
- package/src/debug/logger.mjs +141 -0
- package/src/lsp/client.mjs +2 -2
- package/src/lsp/diagnostic-store.mjs +5 -2
- package/src/lsp/diagnostics-format.mjs +3 -1
- package/src/lsp/managed-node-server.mjs +94 -0
- package/src/lsp/path-match.mjs +10 -0
- package/src/lsp/servers.mjs +56 -13
- package/src/lsp/service.mjs +6 -6
- package/src/lsp/status-message.mjs +1 -0
- package/src/lsp/typescript-project-resolver.mjs +186 -0
- package/src/main.mjs +17 -24
- package/src/platform/spawn-command.mjs +27 -0
- package/src/provider/hosted-tools.mjs +111 -0
- package/src/shell/runtime.mjs +9 -1
- package/src/web/tools.mjs +2 -2
|
@@ -4,6 +4,7 @@ import { renderMarkdown, renderStreamingMarkdown } from "./markdown-renderer.mjs
|
|
|
4
4
|
import { renderEditDiffBlock } from "./tui-diff-rendering.mjs";
|
|
5
5
|
import { OutputScrollState } from "./output/scroll-state.mjs";
|
|
6
6
|
import { appendTextLines, wrapLine } from "./output/text-line-renderer.mjs";
|
|
7
|
+
import { sliceLinesWithTail } from "./output/visible-lines.mjs";
|
|
7
8
|
|
|
8
9
|
const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
9
10
|
|
|
@@ -72,6 +73,7 @@ export class OutputBuffer {
|
|
|
72
73
|
this.overlayStatus = null;
|
|
73
74
|
this.scrollState = new OutputScrollState();
|
|
74
75
|
this._segmentLinesCache = new Map();
|
|
76
|
+
this._baseLinesCache = new Map();
|
|
75
77
|
}
|
|
76
78
|
|
|
77
79
|
get scrollOffset() {
|
|
@@ -89,6 +91,7 @@ export class OutputBuffer {
|
|
|
89
91
|
this.overlayStatus = null;
|
|
90
92
|
this.scrollState.clear();
|
|
91
93
|
this._segmentLinesCache = new Map();
|
|
94
|
+
this._baseLinesCache = new Map();
|
|
92
95
|
}
|
|
93
96
|
|
|
94
97
|
write(text) {
|
|
@@ -101,6 +104,7 @@ export class OutputBuffer {
|
|
|
101
104
|
|
|
102
105
|
_writeText(text, markdown) {
|
|
103
106
|
this.overlayStatus = null;
|
|
107
|
+
this._invalidateBaseLines();
|
|
104
108
|
const current = this.currentText.at(-1);
|
|
105
109
|
if (current.markdown !== markdown && current.text !== "") {
|
|
106
110
|
this.currentText.push({ text: "", markdown });
|
|
@@ -114,6 +118,7 @@ export class OutputBuffer {
|
|
|
114
118
|
|
|
115
119
|
writeln(text) {
|
|
116
120
|
this.overlayStatus = null;
|
|
121
|
+
this._invalidateBaseLines();
|
|
117
122
|
this.currentText[this.currentText.length - 1].text += text;
|
|
118
123
|
this.currentText.push({ text: "", markdown: false });
|
|
119
124
|
}
|
|
@@ -122,6 +127,7 @@ export class OutputBuffer {
|
|
|
122
127
|
const current = this.currentText.at(-1);
|
|
123
128
|
if (!current || current.text === "") return false;
|
|
124
129
|
this.currentText.push({ text: "", markdown: false });
|
|
130
|
+
this._invalidateBaseLines();
|
|
125
131
|
return true;
|
|
126
132
|
}
|
|
127
133
|
|
|
@@ -141,12 +147,14 @@ export class OutputBuffer {
|
|
|
141
147
|
if (lastIdx >= 0) this._activeThinking.content[lastIdx] += parts[0];
|
|
142
148
|
else this._activeThinking.content.push(parts[0]);
|
|
143
149
|
for (let i = 1; i < parts.length; i++) this._activeThinking.content.push(parts[i]);
|
|
150
|
+
this._invalidateBaseLines();
|
|
144
151
|
}
|
|
145
152
|
|
|
146
153
|
endThinking(tokens) {
|
|
147
154
|
if (this._activeThinking) {
|
|
148
155
|
this._activeThinking.tokens = tokens;
|
|
149
156
|
this._activeThinking = null;
|
|
157
|
+
this._invalidateSegmentLines();
|
|
150
158
|
}
|
|
151
159
|
}
|
|
152
160
|
|
|
@@ -166,10 +174,12 @@ export class OutputBuffer {
|
|
|
166
174
|
|
|
167
175
|
setOverlayStatus(lines) {
|
|
168
176
|
this.overlayStatus = Array.isArray(lines) ? { type: "status", lines } : null;
|
|
177
|
+
this._invalidateBaseLines();
|
|
169
178
|
}
|
|
170
179
|
|
|
171
180
|
clearOverlayStatus() {
|
|
172
181
|
this.overlayStatus = null;
|
|
182
|
+
this._invalidateBaseLines();
|
|
173
183
|
}
|
|
174
184
|
|
|
175
185
|
sealCurrentText() {
|
|
@@ -232,34 +242,33 @@ export class OutputBuffer {
|
|
|
232
242
|
|
|
233
243
|
_invalidateSegmentLines() {
|
|
234
244
|
this._segmentLinesCache.clear();
|
|
245
|
+
this._invalidateBaseLines();
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
_invalidateBaseLines() {
|
|
249
|
+
this._baseLinesCache.clear();
|
|
235
250
|
}
|
|
236
251
|
|
|
237
252
|
render(width) {
|
|
238
|
-
const
|
|
239
|
-
this.
|
|
240
|
-
this.scrollState.setTotalLines(
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
253
|
+
const baseLines = this._renderBaseLines(width);
|
|
254
|
+
const tailLine = this.spinning ? this._spinnerLine() : null;
|
|
255
|
+
this.scrollState.setTotalLines(baseLines.length + (tailLine == null ? 0 : 1));
|
|
256
|
+
return sliceLinesWithTail(baseLines, tailLine, this.scrollState.sliceRange());
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
_spinnerLine() {
|
|
260
|
+
return brightBlack(`${SPINNER_FRAMES[this.spinnerIdx]} ${this.spinnerText}`);
|
|
245
261
|
}
|
|
246
262
|
|
|
247
|
-
|
|
263
|
+
_renderBaseLines(width) {
|
|
264
|
+
const cached = this._baseLinesCache.get(width);
|
|
265
|
+
if (cached) return cached;
|
|
248
266
|
const lines = [...this._renderCachedSegmentLines(width)];
|
|
249
267
|
const dynamicStart = this._cachedSegmentPrefixCount();
|
|
250
|
-
for (const seg of this.segments.slice(dynamicStart))
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
for (const line of renderBlock(block, width)) lines.push(line);
|
|
255
|
-
}
|
|
256
|
-
if (this.overlayStatus) {
|
|
257
|
-
for (const line of renderBlock(this.overlayStatus, width)) lines.push(line);
|
|
258
|
-
}
|
|
259
|
-
if (this.spinning) {
|
|
260
|
-
const frame = SPINNER_FRAMES[this.spinnerIdx];
|
|
261
|
-
lines.push(brightBlack(`${frame} ${this.spinnerText}`));
|
|
262
|
-
}
|
|
268
|
+
for (const seg of this.segments.slice(dynamicStart)) for (const line of renderBlock(seg, width)) lines.push(line);
|
|
269
|
+
for (const block of currentTextToBlocks(this.currentText, false, this.currentTextCache)) for (const line of renderBlock(block, width)) lines.push(line);
|
|
270
|
+
if (this.overlayStatus) for (const line of renderBlock(this.overlayStatus, width)) lines.push(line);
|
|
271
|
+
this._baseLinesCache.set(width, lines);
|
|
263
272
|
return lines;
|
|
264
273
|
}
|
|
265
274
|
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export function createStreamDeltaBuffer({
|
|
2
|
+
writeText,
|
|
3
|
+
writeThinking,
|
|
4
|
+
renderSoon,
|
|
5
|
+
delayMs = 16,
|
|
6
|
+
setTimeoutImpl = setTimeout,
|
|
7
|
+
clearTimeoutImpl = clearTimeout,
|
|
8
|
+
} = {}) {
|
|
9
|
+
const queued = [];
|
|
10
|
+
let timer = null;
|
|
11
|
+
|
|
12
|
+
function append(kind, delta) {
|
|
13
|
+
if (!delta) return;
|
|
14
|
+
const last = queued.at(-1);
|
|
15
|
+
if (last?.kind === kind) last.text += delta;
|
|
16
|
+
else queued.push({ kind, text: delta });
|
|
17
|
+
schedule();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function schedule() {
|
|
21
|
+
if (timer) return;
|
|
22
|
+
timer = setTimeoutImpl(() => flush(), delayMs);
|
|
23
|
+
timer.unref?.();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function flush({ notify = true } = {}) {
|
|
27
|
+
if (timer) {
|
|
28
|
+
clearTimeoutImpl(timer);
|
|
29
|
+
timer = null;
|
|
30
|
+
}
|
|
31
|
+
if (!queued.length) return false;
|
|
32
|
+
const batch = queued.splice(0);
|
|
33
|
+
for (const item of batch) {
|
|
34
|
+
if (item.kind === "thinking") writeThinking(item.text);
|
|
35
|
+
else writeText(item.text);
|
|
36
|
+
}
|
|
37
|
+
if (notify) renderSoon();
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
text: (delta) => append("text", delta),
|
|
43
|
+
thinking: (delta) => append("thinking", delta),
|
|
44
|
+
flush,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
@@ -10,16 +10,19 @@ export class ScreenSelection {
|
|
|
10
10
|
this.anchor = null;
|
|
11
11
|
this.focus = null;
|
|
12
12
|
this.lines = [];
|
|
13
|
+
this._plainLines = [];
|
|
13
14
|
this.viewport = { topRow: 0, leftCol: 0, width: Infinity, height: 0 };
|
|
14
15
|
}
|
|
15
16
|
|
|
16
17
|
setLines(lines) {
|
|
17
|
-
this.lines = lines
|
|
18
|
+
this.lines = [...lines];
|
|
19
|
+
this._plainLines = [];
|
|
18
20
|
this.viewport = { topRow: 0, leftCol: 0, width: Infinity, height: this.lines.length };
|
|
19
21
|
}
|
|
20
22
|
|
|
21
23
|
setViewport({ topRow = 0, leftCol = 0, width = Infinity, lines = [] } = {}) {
|
|
22
|
-
this.lines = lines
|
|
24
|
+
this.lines = lines;
|
|
25
|
+
this._plainLines = [];
|
|
23
26
|
this.viewport = {
|
|
24
27
|
topRow: Math.max(0, Math.trunc(topRow)),
|
|
25
28
|
leftCol: Math.max(0, Math.trunc(leftCol)),
|
|
@@ -68,7 +71,7 @@ export class ScreenSelection {
|
|
|
68
71
|
if (!range) return "";
|
|
69
72
|
const selected = [];
|
|
70
73
|
for (let row = range.start.row; row <= range.end.row; row += 1) {
|
|
71
|
-
const line = this.
|
|
74
|
+
const line = this._plainLine(row);
|
|
72
75
|
const startCol = row === range.start.row ? range.start.col : 0;
|
|
73
76
|
const endCol = row === range.end.row ? range.end.col : visibleWidth(line);
|
|
74
77
|
selected.push(sliceColumns(line, startCol, endCol));
|
|
@@ -81,7 +84,7 @@ export class ScreenSelection {
|
|
|
81
84
|
if (!range) return lines;
|
|
82
85
|
return lines.map((line, row) => {
|
|
83
86
|
if (row < range.start.row || row > range.end.row) return line;
|
|
84
|
-
const plain =
|
|
87
|
+
const plain = this._plainLine(row);
|
|
85
88
|
const startCol = row === range.start.row ? range.start.col : 0;
|
|
86
89
|
const endCol = row === range.end.row ? range.end.col : visibleWidth(plain);
|
|
87
90
|
if (endCol <= startCol) return line;
|
|
@@ -89,6 +92,11 @@ export class ScreenSelection {
|
|
|
89
92
|
});
|
|
90
93
|
}
|
|
91
94
|
|
|
95
|
+
_plainLine(row) {
|
|
96
|
+
if (this._plainLines[row] == null) this._plainLines[row] = stripAnsi(this.lines[row] ?? "");
|
|
97
|
+
return this._plainLines[row];
|
|
98
|
+
}
|
|
99
|
+
|
|
92
100
|
range() {
|
|
93
101
|
if (!this.anchor || !this.focus) return null;
|
|
94
102
|
const [start, end] = comparePoints(this.anchor, this.focus) <= 0
|
|
@@ -55,7 +55,7 @@ export function formatToolStartLine(name, args = {}) {
|
|
|
55
55
|
if (name === "command_exec") return joinToolParts("◆", name, [compactText(args?.command ?? "")]);
|
|
56
56
|
if (name === "terminal_send") return joinToolParts("◆", name, [args?.shell_id, formatTerminalSendAction(args)]);
|
|
57
57
|
if (name?.startsWith?.("terminal_")) return joinToolParts("◆", name, [args?.shell_id, formatTerminalDetails(args)]);
|
|
58
|
-
if (name === "
|
|
58
|
+
if (name === "external_web_search") return joinToolParts("◆", name, [quoteCompact(args?.query ?? "")]);
|
|
59
59
|
if (name === "web_fetch") return joinToolParts("◆", name, [compactText(args?.url ?? "")]);
|
|
60
60
|
if (name === "context_stats") return joinToolParts("◆", name, []);
|
|
61
61
|
if (name === "read") {
|
package/src/cli/ui.mjs
CHANGED
|
@@ -24,6 +24,7 @@ import { writeMemoryHint } 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";
|
|
27
|
+
import { createStreamDeltaBuffer } from "./tui/render/stream-delta-buffer.mjs";
|
|
27
28
|
import { writeTranscriptToOutput } from "../session/transcript.mjs";
|
|
28
29
|
|
|
29
30
|
export { buildMarchCommands, MarchAutocompleteProvider } from "./input/autocomplete.mjs";
|
|
@@ -61,8 +62,9 @@ export function createTuiUI({
|
|
|
61
62
|
let toolsExpanded = false;
|
|
62
63
|
const activeToolBlocks = [];
|
|
63
64
|
const renderScheduler = createRenderScheduler({ requestRender: () => tui.requestRender() });
|
|
64
|
-
const
|
|
65
|
-
|
|
65
|
+
const streamDeltas = createStreamDeltaBuffer({ writeText: (delta) => output.writeMarkdown(delta), writeThinking: (delta) => output.appendThinking(delta), renderSoon: renderScheduler.renderSoon });
|
|
66
|
+
const flushStreamDeltas = () => streamDeltas.flush({ notify: false });
|
|
67
|
+
const requestRender = () => { flushStreamDeltas(); renderScheduler.renderNow(); };
|
|
66
68
|
const spinnerStatus = createSpinnerStatusController({ output, requestRender });
|
|
67
69
|
const retryStatus = createRetryStatusController({ output, requestRender, stopSpinner: spinnerStatus.stop });
|
|
68
70
|
const shellDrawerControls = createShellDrawerControls({ shellDrawer, output, requestRender });
|
|
@@ -169,12 +171,10 @@ export function createTuiUI({
|
|
|
169
171
|
retryStatus.stop(); output.startThinking(); requestRender();
|
|
170
172
|
},
|
|
171
173
|
|
|
172
|
-
thinkingDelta: (delta) =>
|
|
173
|
-
output.appendThinking(delta);
|
|
174
|
-
renderScheduler.renderSoon();
|
|
175
|
-
},
|
|
174
|
+
thinkingDelta: (delta) => streamDeltas.thinking(delta),
|
|
176
175
|
|
|
177
176
|
thinkingEnd: (tokens) => {
|
|
177
|
+
flushStreamDeltas();
|
|
178
178
|
output.endThinking(tokens);
|
|
179
179
|
requestRender();
|
|
180
180
|
},
|
|
@@ -186,7 +186,7 @@ export function createTuiUI({
|
|
|
186
186
|
toggleLastThinking: () => false,
|
|
187
187
|
|
|
188
188
|
toolStart: (name, args) => {
|
|
189
|
-
ensureStarted(); retryStatus.stop(); spinnerStatus.stop(); activeToolBlocks.push(writeToolStart({ output, name, args })); requestRender();
|
|
189
|
+
ensureStarted(); flushStreamDeltas(); retryStatus.stop(); spinnerStatus.stop(); activeToolBlocks.push(writeToolStart({ output, name, args })); requestRender();
|
|
190
190
|
},
|
|
191
191
|
|
|
192
192
|
toolEnd: (name, isError, result) => {
|
|
@@ -194,30 +194,26 @@ export function createTuiUI({
|
|
|
194
194
|
},
|
|
195
195
|
|
|
196
196
|
textDelta: (delta) => {
|
|
197
|
-
ensureStarted(); retryStatus.stop(); spinnerStatus.stop();
|
|
198
|
-
output.writeMarkdown(delta);
|
|
199
|
-
renderScheduler.renderSoon();
|
|
197
|
+
ensureStarted(); retryStatus.stop(); spinnerStatus.stop(); streamDeltas.text(delta);
|
|
200
198
|
},
|
|
201
199
|
assistantReplyEnd: () => {
|
|
202
200
|
ensureStarted();
|
|
201
|
+
flushStreamDeltas();
|
|
203
202
|
const changed = output.ensureNewline();
|
|
204
203
|
if (output.sealCurrentText() || changed) requestRender();
|
|
205
204
|
},
|
|
206
205
|
status: (text) => {
|
|
207
|
-
ensureStarted(); retryStatus.stop(); spinnerStatus.stop(); output.setOverlayStatus([brightBlack(`● ${text}`)]); requestRender();
|
|
206
|
+
ensureStarted(); flushStreamDeltas(); retryStatus.stop(); spinnerStatus.stop(); output.setOverlayStatus([brightBlack(`● ${text}`)]); requestRender();
|
|
208
207
|
},
|
|
209
208
|
memoryHint: ({ hints }) => {
|
|
210
|
-
ensureStarted(); retryStatus.stop(); spinnerStatus.stop(); output.ensureNewline(); writeMemoryHint({ output, hints }); requestRender();
|
|
209
|
+
ensureStarted(); flushStreamDeltas(); retryStatus.stop(); spinnerStatus.stop(); output.ensureNewline(); writeMemoryHint({ output, hints }); requestRender();
|
|
211
210
|
},
|
|
212
211
|
|
|
213
212
|
clearOutput: () => {
|
|
214
|
-
ensureStarted(); spinnerStatus.stop(); retryStatus.stop(); output.clear(); requestRender();
|
|
213
|
+
ensureStarted(); flushStreamDeltas(); spinnerStatus.stop(); retryStatus.stop(); output.clear(); requestRender();
|
|
215
214
|
},
|
|
216
|
-
|
|
217
215
|
restoreTranscript: (turns) => {
|
|
218
|
-
ensureStarted(); spinnerStatus.stop(); retryStatus.stop(); output.clear();
|
|
219
|
-
writeTranscriptToOutput(output, turns);
|
|
220
|
-
requestRender();
|
|
216
|
+
ensureStarted(); flushStreamDeltas(); spinnerStatus.stop(); retryStatus.stop(); output.clear(); writeTranscriptToOutput(output, turns); requestRender();
|
|
221
217
|
},
|
|
222
218
|
|
|
223
219
|
setStatusBar: (text) => {
|
|
@@ -229,6 +225,7 @@ export function createTuiUI({
|
|
|
229
225
|
},
|
|
230
226
|
|
|
231
227
|
turnEnd: () => {
|
|
228
|
+
flushStreamDeltas();
|
|
232
229
|
const changed = output.ensureNewline();
|
|
233
230
|
if (output.sealCurrentText() || changed) requestRender();
|
|
234
231
|
},
|
|
@@ -238,6 +235,7 @@ export function createTuiUI({
|
|
|
238
235
|
|
|
239
236
|
editDiff: (path, diffLines) => {
|
|
240
237
|
ensureStarted();
|
|
238
|
+
flushStreamDeltas();
|
|
241
239
|
spinnerStatus.stop();
|
|
242
240
|
writeEditDiff({ output, path, diffLines });
|
|
243
241
|
requestRender();
|
|
@@ -278,6 +276,7 @@ export function createTuiUI({
|
|
|
278
276
|
toggleShellDrawer: () => shellDrawerControls.toggle(),
|
|
279
277
|
requestExit: () => inputController.requestExit(),
|
|
280
278
|
close: async () => {
|
|
279
|
+
flushStreamDeltas();
|
|
281
280
|
renderScheduler.clearPending();
|
|
282
281
|
spinnerStatus.stop();
|
|
283
282
|
retryStatus.stop();
|
package/src/config/loader.mjs
CHANGED
|
@@ -46,13 +46,20 @@ function mergeLayers(layers) {
|
|
|
46
46
|
serviceTier: null,
|
|
47
47
|
providers: {},
|
|
48
48
|
webSearch: { provider: null, providers: {} },
|
|
49
|
+
hostedTools: {
|
|
50
|
+
openai: { webSearch: "auto" },
|
|
51
|
+
openaiCodex: { webSearch: "auto" },
|
|
52
|
+
azureOpenai: { webSearch: "auto" },
|
|
53
|
+
anthropic: { webSearch: "auto" },
|
|
54
|
+
google: { webSearch: "auto" },
|
|
55
|
+
xai: { webSearch: "auto", xSearch: "auto" },
|
|
56
|
+
},
|
|
49
57
|
network: { proxy: "system", ca: "system" },
|
|
50
58
|
maxTurns: null,
|
|
51
59
|
trimBatch: null,
|
|
52
60
|
memoryRoot: null,
|
|
53
61
|
notifications: { turnEnd: true, desktop: true, bell: false, command: null, minDurationMs: 0, sound: true },
|
|
54
62
|
};
|
|
55
|
-
|
|
56
63
|
for (const layer of layers) {
|
|
57
64
|
if (!layer) continue;
|
|
58
65
|
if (layer.model != null) result.model = layer.model;
|
|
@@ -64,6 +71,9 @@ function mergeLayers(layers) {
|
|
|
64
71
|
if (layer.webSearch && typeof layer.webSearch === "object" && !Array.isArray(layer.webSearch)) {
|
|
65
72
|
result.webSearch = mergeWebSearch(result.webSearch, layer.webSearch);
|
|
66
73
|
}
|
|
74
|
+
if (layer.hostedTools && typeof layer.hostedTools === "object" && !Array.isArray(layer.hostedTools)) {
|
|
75
|
+
result.hostedTools = mergeHostedTools(result.hostedTools, layer.hostedTools);
|
|
76
|
+
}
|
|
67
77
|
if (layer.notifications && typeof layer.notifications === "object" && !Array.isArray(layer.notifications)) {
|
|
68
78
|
result.notifications = {
|
|
69
79
|
...result.notifications,
|
|
@@ -99,6 +109,23 @@ function mergeWebSearch(current, next) {
|
|
|
99
109
|
return merged;
|
|
100
110
|
}
|
|
101
111
|
|
|
112
|
+
function mergeHostedTools(current, next) {
|
|
113
|
+
const merged = {
|
|
114
|
+
openai: { ...(current.openai ?? {}) },
|
|
115
|
+
openaiCodex: { ...(current.openaiCodex ?? {}) },
|
|
116
|
+
azureOpenai: { ...(current.azureOpenai ?? {}) },
|
|
117
|
+
anthropic: { ...(current.anthropic ?? {}) },
|
|
118
|
+
google: { ...(current.google ?? {}) },
|
|
119
|
+
xai: { ...(current.xai ?? {}) },
|
|
120
|
+
};
|
|
121
|
+
for (const provider of ["openai", "openaiCodex", "azureOpenai", "anthropic", "google", "xai"]) {
|
|
122
|
+
if (next[provider] && typeof next[provider] === "object" && !Array.isArray(next[provider])) {
|
|
123
|
+
merged[provider] = { ...merged[provider], ...next[provider] };
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return merged;
|
|
127
|
+
}
|
|
128
|
+
|
|
102
129
|
function mergeProviders(current, next) {
|
|
103
130
|
const merged = { ...current };
|
|
104
131
|
for (const [id, profile] of Object.entries(next)) {
|
|
@@ -58,7 +58,7 @@ The user primarily asks for software engineering work: fixing bugs, adding behav
|
|
|
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
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
|
- 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.
|
|
61
|
-
- Distinguish "migrating a Skill to memory" from "learning a Skill": migration preserves the complete Skill folder under memory_root/skills/ and creates a memory index
|
|
61
|
+
- 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.
|
|
62
62
|
- 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.
|
|
63
63
|
- 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.
|
|
64
64
|
</memory_system>
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { appendFileSync, mkdirSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
|
|
5
|
+
const LEVELS = Object.freeze({ debug: 10, info: 20, warn: 30, error: 40, silent: 99 });
|
|
6
|
+
const REDACTED = "[redacted]";
|
|
7
|
+
const MAX_STRING_LENGTH = 2000;
|
|
8
|
+
const MAX_ARRAY_LENGTH = 50;
|
|
9
|
+
const MAX_OBJECT_KEYS = 80;
|
|
10
|
+
const SENSITIVE_KEY = /(api[-_]?key|authorization|auth|token|secret|password|cookie|credential|b64|base64|image)/i;
|
|
11
|
+
|
|
12
|
+
export function createLogger({
|
|
13
|
+
enabled = process.env.MARCH_LOG !== "0",
|
|
14
|
+
level = process.env.MARCH_LOG_LEVEL ?? "info",
|
|
15
|
+
logDir = defaultLogDir(),
|
|
16
|
+
now = () => new Date(),
|
|
17
|
+
pid = process.pid,
|
|
18
|
+
} = {}) {
|
|
19
|
+
const threshold = normalizeLevel(level);
|
|
20
|
+
const path = join(logDir, `${dateStamp(now())}-march-${pid}.jsonl`);
|
|
21
|
+
const base = { enabled: Boolean(enabled), level: levelName(threshold), path };
|
|
22
|
+
|
|
23
|
+
function write(levelNameValue, event, fields = {}) {
|
|
24
|
+
if (!base.enabled || normalizeLevel(levelNameValue) < threshold) return;
|
|
25
|
+
const entry = {
|
|
26
|
+
ts: now().toISOString(),
|
|
27
|
+
level: levelNameValue,
|
|
28
|
+
event,
|
|
29
|
+
pid,
|
|
30
|
+
...sanitize(fields),
|
|
31
|
+
};
|
|
32
|
+
try {
|
|
33
|
+
mkdirSync(logDir, { recursive: true });
|
|
34
|
+
appendFileSync(path, `${JSON.stringify(entry)}\n`, "utf8");
|
|
35
|
+
} catch {
|
|
36
|
+
// Logging must never change CLI behavior.
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const logger = {
|
|
41
|
+
...base,
|
|
42
|
+
event: (event, fields) => write("info", event, fields),
|
|
43
|
+
debug: (event, fields) => write("debug", event, fields),
|
|
44
|
+
warn: (event, fields) => write("warn", event, fields),
|
|
45
|
+
error: (event, fields) => write("error", event, fields),
|
|
46
|
+
child(extraFields = {}) {
|
|
47
|
+
return createChildLogger(logger, sanitize(extraFields));
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
return logger;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function createHeartbeat({ logger, event = "heartbeat", intervalMs = 10_000, getFields = () => ({}) } = {}) {
|
|
54
|
+
if (!logger?.enabled || intervalMs <= 0) return { stop() {} };
|
|
55
|
+
const timer = setInterval(() => logger.event(event, getFields()), intervalMs);
|
|
56
|
+
timer.unref?.();
|
|
57
|
+
return {
|
|
58
|
+
stop() { clearInterval(timer); },
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function installProcessLogHandlers(logger) {
|
|
63
|
+
if (!logger?.enabled) return;
|
|
64
|
+
process.once("uncaughtException", (err) => {
|
|
65
|
+
logger.error("process.uncaughtException", { error: formatError(err) });
|
|
66
|
+
});
|
|
67
|
+
process.once("unhandledRejection", (reason) => {
|
|
68
|
+
logger.error("process.unhandledRejection", { error: formatError(reason) });
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function formatError(err) {
|
|
73
|
+
if (err instanceof Error) {
|
|
74
|
+
return {
|
|
75
|
+
name: err.name,
|
|
76
|
+
message: err.message,
|
|
77
|
+
stack: err.stack,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
return { message: String(err) };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function sanitize(value, seen = new WeakSet(), key = "") {
|
|
84
|
+
if (SENSITIVE_KEY.test(key)) return REDACTED;
|
|
85
|
+
if (value == null || typeof value === "number" || typeof value === "boolean") return value;
|
|
86
|
+
if (typeof value === "string") return truncateString(value);
|
|
87
|
+
if (typeof value === "bigint") return String(value);
|
|
88
|
+
if (typeof value === "function" || typeof value === "symbol") return `[${typeof value}]`;
|
|
89
|
+
if (value instanceof Error) return sanitize(formatError(value), seen);
|
|
90
|
+
if (typeof value !== "object") return String(value);
|
|
91
|
+
if (seen.has(value)) return "[circular]";
|
|
92
|
+
seen.add(value);
|
|
93
|
+
if (Array.isArray(value)) {
|
|
94
|
+
const items = value.slice(0, MAX_ARRAY_LENGTH).map((item) => sanitize(item, seen));
|
|
95
|
+
if (value.length > MAX_ARRAY_LENGTH) items.push(`[${value.length - MAX_ARRAY_LENGTH} more items]`);
|
|
96
|
+
return items;
|
|
97
|
+
}
|
|
98
|
+
const entries = Object.entries(value).slice(0, MAX_OBJECT_KEYS);
|
|
99
|
+
const out = {};
|
|
100
|
+
for (const [entryKey, entryValue] of entries) out[entryKey] = sanitize(entryValue, seen, entryKey);
|
|
101
|
+
const omitted = Object.keys(value).length - entries.length;
|
|
102
|
+
if (omitted > 0) out.__omittedKeys = omitted;
|
|
103
|
+
return out;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function createChildLogger(parent, extraFields) {
|
|
107
|
+
function withFields(fields) {
|
|
108
|
+
return { ...extraFields, ...(fields ?? {}) };
|
|
109
|
+
}
|
|
110
|
+
return {
|
|
111
|
+
enabled: parent.enabled,
|
|
112
|
+
level: parent.level,
|
|
113
|
+
path: parent.path,
|
|
114
|
+
event: (event, fields) => parent.event(event, withFields(fields)),
|
|
115
|
+
debug: (event, fields) => parent.debug(event, withFields(fields)),
|
|
116
|
+
warn: (event, fields) => parent.warn(event, withFields(fields)),
|
|
117
|
+
error: (event, fields) => parent.error(event, withFields(fields)),
|
|
118
|
+
child: (fields = {}) => createChildLogger(parent, withFields(sanitize(fields))),
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function defaultLogDir() {
|
|
123
|
+
return join(homedir(), ".march", "logs");
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function dateStamp(now) {
|
|
127
|
+
return now.toISOString().slice(0, 10);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function truncateString(value) {
|
|
131
|
+
if (value.length <= MAX_STRING_LENGTH) return value;
|
|
132
|
+
return `${value.slice(0, MAX_STRING_LENGTH)}...[truncated ${value.length - MAX_STRING_LENGTH} chars]`;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function normalizeLevel(level) {
|
|
136
|
+
return LEVELS[String(level).toLowerCase()] ?? LEVELS.info;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function levelName(value) {
|
|
140
|
+
return Object.entries(LEVELS).find(([, level]) => level === value)?.[0] ?? "info";
|
|
141
|
+
}
|
package/src/lsp/client.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { spawn } from "node:child_process";
|
|
2
1
|
import { pathToFileURL } from "node:url";
|
|
2
|
+
import { spawnCommand } from "../platform/spawn-command.mjs";
|
|
3
3
|
import { extname } from "node:path";
|
|
4
4
|
import { readFileSync } from "node:fs";
|
|
5
5
|
|
|
@@ -36,7 +36,7 @@ export class LspClient {
|
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
async start() {
|
|
39
|
-
this.process =
|
|
39
|
+
this.process = spawnCommand(this.command, this.args, {
|
|
40
40
|
cwd: this.cwd,
|
|
41
41
|
env: process.env,
|
|
42
42
|
stdio: ["pipe", "pipe", "pipe"],
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { fileURLToPath } from "node:url";
|
|
2
|
+
import { lspPathKey } from "./path-match.mjs";
|
|
2
3
|
|
|
3
4
|
export class LspDiagnosticStore {
|
|
4
5
|
constructor() {
|
|
@@ -13,7 +14,7 @@ export class LspDiagnosticStore {
|
|
|
13
14
|
serverId,
|
|
14
15
|
path,
|
|
15
16
|
}));
|
|
16
|
-
this.byPath.set(path, {
|
|
17
|
+
this.byPath.set(lspPathKey(path), {
|
|
17
18
|
path,
|
|
18
19
|
updatedAt: Date.now(),
|
|
19
20
|
diagnostics: normalized,
|
|
@@ -22,10 +23,12 @@ export class LspDiagnosticStore {
|
|
|
22
23
|
|
|
23
24
|
snapshot() {
|
|
24
25
|
const diagnostics = [];
|
|
26
|
+
const files = [];
|
|
25
27
|
for (const entry of this.byPath.values()) {
|
|
26
28
|
diagnostics.push(...entry.diagnostics);
|
|
29
|
+
files.push({ path: entry.path, updatedAt: entry.updatedAt, diagnostics: entry.diagnostics.length });
|
|
27
30
|
}
|
|
28
|
-
return diagnostics;
|
|
31
|
+
return { diagnostics, files };
|
|
29
32
|
}
|
|
30
33
|
}
|
|
31
34
|
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { sameLspPath } from "./path-match.mjs";
|
|
2
|
+
|
|
1
3
|
const MAX_DIAGNOSTICS = 20;
|
|
2
4
|
|
|
3
5
|
export function formatLspDiagnostics({ snapshot } = {}) {
|
|
@@ -20,7 +22,7 @@ export function formatLspDiagnostics({ snapshot } = {}) {
|
|
|
20
22
|
export function formatLspDiagnosticsForPath({ snapshot, path } = {}) {
|
|
21
23
|
const targetPath = String(path ?? "");
|
|
22
24
|
if (!targetPath) return "";
|
|
23
|
-
const diagnostics = (snapshot?.diagnostics ?? []).filter((diagnostic) => diagnostic.path
|
|
25
|
+
const diagnostics = (snapshot?.diagnostics ?? []).filter((diagnostic) => sameLspPath(diagnostic.path, targetPath));
|
|
24
26
|
if (diagnostics.length === 0) return "";
|
|
25
27
|
return formatLspDiagnostics({
|
|
26
28
|
snapshot: {
|