march-cli 0.1.23 → 0.1.24
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/README.md +1 -1
- package/README.zh.md +1 -1
- package/package.json +7 -1
- package/src/agent/editing/cohesion-warning.mjs +57 -0
- package/src/agent/file-edit-tool.mjs +27 -12
package/README.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<p align="center">
|
|
2
|
-
<img src="docs/assets/march-banner.
|
|
2
|
+
<img src="docs/assets/march-banner.svg" alt="March CLI banner" width="800">
|
|
3
3
|
</p>
|
|
4
4
|
|
|
5
5
|
<p align="center"><strong>Forget Skills. Embrace Memory. Keep your model in the sweet spot.</strong></p>
|
package/README.zh.md
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "march-cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.24",
|
|
4
4
|
"description": "March CLI — terminal-native coding agent with context reconstruction",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/main.mjs",
|
|
@@ -19,6 +19,9 @@
|
|
|
19
19
|
"test:shell-runtime-real": "node test/shell-real-runtime.acceptance.mjs",
|
|
20
20
|
"test:shell-tui-real": "node test/shell-tui-real.acceptance.mjs",
|
|
21
21
|
"test:tui-key-real": "node test/tui-key-real.acceptance.mjs",
|
|
22
|
+
"docs:dev": "vitepress dev docs --host 127.0.0.1",
|
|
23
|
+
"docs:build": "vitepress build docs",
|
|
24
|
+
"docs:preview": "vitepress preview docs --host 127.0.0.1",
|
|
22
25
|
"context": "cd .. && node march-cli/bin/march.mjs --dump-context",
|
|
23
26
|
"notify:experiment": "node scripts/notify-experiment.mjs",
|
|
24
27
|
"publish:env": "node scripts/npm-publish-from-env.mjs"
|
|
@@ -39,5 +42,8 @@
|
|
|
39
42
|
},
|
|
40
43
|
"optionalDependencies": {
|
|
41
44
|
"@vscode/ripgrep": "^1.18.0"
|
|
45
|
+
},
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"vitepress": "^1.6.4"
|
|
42
48
|
}
|
|
43
49
|
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { extname, normalize } from "node:path";
|
|
2
|
+
|
|
3
|
+
const MAX_COHESIVE_LINES = 300;
|
|
4
|
+
const MIN_ADDED_LINES = 40;
|
|
5
|
+
|
|
6
|
+
const CODE_EXTENSIONS = new Set([
|
|
7
|
+
".cjs",
|
|
8
|
+
".go",
|
|
9
|
+
".js",
|
|
10
|
+
".jsx",
|
|
11
|
+
".mjs",
|
|
12
|
+
".py",
|
|
13
|
+
".rs",
|
|
14
|
+
".ts",
|
|
15
|
+
".tsx",
|
|
16
|
+
]);
|
|
17
|
+
|
|
18
|
+
const IGNORED_SEGMENTS = new Set([
|
|
19
|
+
"build",
|
|
20
|
+
"coverage",
|
|
21
|
+
"dist",
|
|
22
|
+
"fixture",
|
|
23
|
+
"fixtures",
|
|
24
|
+
"node_modules",
|
|
25
|
+
"vendor",
|
|
26
|
+
]);
|
|
27
|
+
|
|
28
|
+
export function buildCohesionWarning({ path, oldText, newText }) {
|
|
29
|
+
if (!shouldCheckPath(path)) return null;
|
|
30
|
+
|
|
31
|
+
const oldLines = countLines(oldText);
|
|
32
|
+
const newLines = countLines(newText);
|
|
33
|
+
const addedLines = Math.max(0, newLines - oldLines);
|
|
34
|
+
if (newLines <= MAX_COHESIVE_LINES || addedLines < MIN_ADDED_LINES) return null;
|
|
35
|
+
|
|
36
|
+
return [
|
|
37
|
+
"[cohesion]",
|
|
38
|
+
`${formatPath(path)} is now ${newLines} lines after a +${addedLines} line edit.`,
|
|
39
|
+
"If the added logic is a separate responsibility, extract it into a cohesive module before continuing.",
|
|
40
|
+
].join("\n");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function shouldCheckPath(path) {
|
|
44
|
+
const normalized = normalize(path).replaceAll("\\", "/");
|
|
45
|
+
if (normalized.includes(".generated.")) return false;
|
|
46
|
+
if (!CODE_EXTENSIONS.has(extname(normalized))) return false;
|
|
47
|
+
return !normalized.split("/").some((segment) => IGNORED_SEGMENTS.has(segment));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function countLines(text) {
|
|
51
|
+
if (!text) return 0;
|
|
52
|
+
return String(text).split("\n").length;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function formatPath(path) {
|
|
56
|
+
return String(path).replaceAll("\\", "/");
|
|
57
|
+
}
|
|
@@ -4,8 +4,9 @@ import { defineTool } from "@earendil-works/pi-coding-agent";
|
|
|
4
4
|
import { Type } from "typebox";
|
|
5
5
|
import { toolText } from "./tool-result.mjs";
|
|
6
6
|
import { applyReplaceTextPatch, applyReplaceRangePatch } from "./editing/diff-apply.mjs";
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
7
|
+
import { buildCohesionWarning } from "./editing/cohesion-warning.mjs";
|
|
8
|
+
import { formatAppliedDiff, formatDiff } from "./editing/diff-format.mjs";
|
|
9
|
+
import { waitForLspReport } from "./editing/lsp-report.mjs";
|
|
9
10
|
|
|
10
11
|
export { formatDiff } from "./editing/diff-format.mjs";
|
|
11
12
|
|
|
@@ -67,8 +68,9 @@ async function writeFullFile({ absPath, path, content, mode, engine, ui, lspServ
|
|
|
67
68
|
if (mode === WRITE_MODE && existsSync(absPath)) {
|
|
68
69
|
return toolText(`Error: ${absPath} already exists. Use mode=overwrite to replace it.`, { error: true });
|
|
69
70
|
}
|
|
70
|
-
const oldText = mode === OVERWRITE_MODE && existsSync(absPath) ? readFileSync(absPath, "utf8") : "";
|
|
71
|
-
const
|
|
71
|
+
const oldText = mode === OVERWRITE_MODE && existsSync(absPath) ? readFileSync(absPath, "utf8") : "";
|
|
72
|
+
const cohesionWarning = buildCohesionWarning({ path: absPath, oldText, newText: content });
|
|
73
|
+
const diffLines = formatDiff(oldText, content, { startLine: 1 });
|
|
72
74
|
try {
|
|
73
75
|
mkdirSync(dirname(absPath), { recursive: true });
|
|
74
76
|
writeFileSync(absPath, content, "utf8");
|
|
@@ -76,7 +78,8 @@ async function writeFullFile({ absPath, path, content, mode, engine, ui, lspServ
|
|
|
76
78
|
const lspResult = await lspService?.touchFile?.(absPath);
|
|
77
79
|
ui.editDiff(absPath, diffLines);
|
|
78
80
|
|
|
79
|
-
|
|
81
|
+
const baseText = `${mode === WRITE_MODE ? "Wrote" : "Overwrote"} ${path}\n\n${formatAppliedDiff([{ oldText, newText: content, startLine: 1 }])}`;
|
|
82
|
+
return await toolTextWithDiagnostics(appendSections(baseText, [cohesionWarning]), withWarnings({ path: absPath }, cohesionWarning), { lspService, path: absPath, lspResult, since: lspTouchedAt });
|
|
80
83
|
} catch (err) {
|
|
81
84
|
return toolText(`Error writing ${absPath}: ${err.message}`, { error: true });
|
|
82
85
|
}
|
|
@@ -97,23 +100,35 @@ async function patchFile({ absPath, path, edits, engine, ui, lspService }) {
|
|
|
97
100
|
const prepared = preparePatchEdits(content, edits, absPath);
|
|
98
101
|
if (prepared.error) return toolText(prepared.error, { error: true });
|
|
99
102
|
|
|
100
|
-
const newContent = applyPreparedEdits(content, prepared.edits);
|
|
101
|
-
|
|
103
|
+
const newContent = applyPreparedEdits(content, prepared.edits);
|
|
104
|
+
const cohesionWarning = buildCohesionWarning({ path: absPath, oldText: content, newText: newContent });
|
|
105
|
+
try {
|
|
102
106
|
mkdirSync(dirname(absPath), { recursive: true });
|
|
103
107
|
writeFileSync(absPath, newContent, "utf8");
|
|
104
108
|
const lspTouchedAt = Date.now();
|
|
105
109
|
const lspResult = await lspService?.touchFile?.(absPath);
|
|
106
110
|
ui.editDiff(absPath, prepared.edits.flatMap((edit) => formatDiff(edit.oldText, edit.newText, { startLine: edit.startLine })));
|
|
107
|
-
|
|
111
|
+
const baseText = `Edited ${absPath}\n\n${formatAppliedDiff(prepared.edits)}`;
|
|
112
|
+
const details = withWarnings({ path: absPath, edits: prepared.edits.length }, cohesionWarning);
|
|
113
|
+
return await toolTextWithDiagnostics(appendSections(baseText, [cohesionWarning]), details, { lspService, path: absPath, lspResult, since: lspTouchedAt });
|
|
108
114
|
} catch (err) {
|
|
109
115
|
return toolText(`Error writing ${absPath}: ${err.message}`, { error: true });
|
|
110
116
|
}
|
|
111
117
|
}
|
|
112
118
|
|
|
113
|
-
async function toolTextWithDiagnostics(text, details, { lspService, path, lspResult, since = Date.now(), timeoutMs = 3000, intervalMs = 150 } = {}) {
|
|
114
|
-
const lspReport = await waitForLspReport({ lspService, path, lspResult, since, timeoutMs, intervalMs });
|
|
115
|
-
return toolText(lspReport ? `${text}\n\n${lspReport}` : text, details);
|
|
116
|
-
}
|
|
119
|
+
async function toolTextWithDiagnostics(text, details, { lspService, path, lspResult, since = Date.now(), timeoutMs = 3000, intervalMs = 150 } = {}) {
|
|
120
|
+
const lspReport = await waitForLspReport({ lspService, path, lspResult, since, timeoutMs, intervalMs });
|
|
121
|
+
return toolText(lspReport ? `${text}\n\n${lspReport}` : text, details);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function appendSections(text, sections) {
|
|
125
|
+
return [text, ...sections.filter(Boolean)].join("\n\n");
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function withWarnings(details, cohesionWarning) {
|
|
129
|
+
if (!cohesionWarning) return details;
|
|
130
|
+
return { ...details, warnings: ["cohesion"] };
|
|
131
|
+
}
|
|
117
132
|
|
|
118
133
|
export function preparePatchEdits(content, edits, path = "file") {
|
|
119
134
|
const prepared = [];
|