@happy-nut/monacori 0.1.3 → 0.1.5
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 +14 -48
- package/dist/app-main.js +47 -7
- package/dist/build.js +23 -1
- package/dist/commands.js +12 -326
- package/dist/i18n.js +10 -0
- package/dist/preload.cjs +9 -0
- package/dist/render.d.ts +11 -0
- package/dist/render.js +13 -5
- package/dist/server.js +6 -0
- package/dist/types.d.ts +12 -0
- package/dist/viewer.client.js +261 -28
- package/dist/viewer.css +56 -7
- package/package.json +1 -1
- package/assets/screenshots/diff-review.png +0 -0
- package/assets/screenshots/terminal.png +0 -0
package/README.md
CHANGED
|
@@ -1,24 +1,10 @@
|
|
|
1
1
|
# monacori
|
|
2
2
|
|
|
3
|
-
**
|
|
4
|
-
|
|
5
|
-
It is *not* an agent orchestrator. It does not manage panes, sessions, worktrees, or model adapters. Its job is narrow on purpose: validate what the AI just did.
|
|
3
|
+
**A local desktop diff-review app for AI-generated code changes.** After an AI edits your repo, run `mo` to open a side-by-side diff — read it, comment on it, and send your comments straight to an AI CLI running in the built-in terminal.
|
|
6
4
|
|
|
7
5
|
## Why
|
|
8
6
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
The loop is simple:
|
|
12
|
-
|
|
13
|
-
```text
|
|
14
|
-
AI edits code.
|
|
15
|
-
monacori runs verification.
|
|
16
|
-
monacori creates a reviewable diff artifact.
|
|
17
|
-
You inspect the evidence before accepting the change.
|
|
18
|
-
```
|
|
19
|
-
|
|
20
|
-

|
|
21
|
-
*Local desktop review app: side-by-side diff with changed lines highlighted and IntelliJ-style git status colors in the sidebar.*
|
|
7
|
+
A chat log or a "done" claim is a poor way to review what an AI changed. monacori puts the change in front of you as a real diff you can read and annotate — then turns your comments into a prompt you hand right back to `claude` or `codex`, without leaving the app or copy-pasting between windows.
|
|
22
8
|
|
|
23
9
|
## Install
|
|
24
10
|
|
|
@@ -30,21 +16,16 @@ After install, the short command is `mo`. A Homebrew tap (`happy-nut/monacori/mo
|
|
|
30
16
|
|
|
31
17
|
## What you get
|
|
32
18
|
|
|
33
|
-
- **
|
|
34
|
-
- **Integrated terminal** — run AI CLIs like `claude` or `codex` inside the app,
|
|
35
|
-
- **
|
|
36
|
-
- **Verification logs & reports** — repeatable verification under `.monacori/logs/` and compact validation reports under `.monacori/reports/`.
|
|
37
|
-
|
|
38
|
-

|
|
39
|
-
*Run your AI CLI inside the review app, next to the diff it just produced.*
|
|
19
|
+
- **Desktop diff review** — side-by-side diff with changed-line highlighting and an IntelliJ-style sidebar that colors files by git status. Reads the repo directly, refreshes on change, no HTTP server.
|
|
20
|
+
- **Integrated terminal** — run AI CLIs like `claude` or `codex` right inside the app, split into panes.
|
|
21
|
+
- **Comments → session** — annotate any line, then send your comments (bundled with their code context) into a terminal pane as one merged prompt: pick the target pane visually and press Enter.
|
|
40
22
|
|
|
41
23
|
## Quick start
|
|
42
24
|
|
|
43
|
-
Inside the repository you want to
|
|
25
|
+
Inside the repository you want to review:
|
|
44
26
|
|
|
45
27
|
```bash
|
|
46
|
-
mo
|
|
47
|
-
monacori check --include-untracked # run verification, write a log, diff, and report
|
|
28
|
+
mo
|
|
48
29
|
```
|
|
49
30
|
|
|
50
31
|
On first run, `mo` creates `.monacori/`, adds it to `.gitignore`, and includes untracked files so new AI-created files show up immediately.
|
|
@@ -53,36 +34,21 @@ On first run, `mo` creates `.monacori/`, adds it to `.gitignore`, and includes u
|
|
|
53
34
|
|
|
54
35
|
| Command | What it does |
|
|
55
36
|
| --- | --- |
|
|
56
|
-
| `mo` | Open the desktop review app (alias for `monacori open`). |
|
|
57
|
-
| `monacori check` | Run verification, then create a diff and a validation report. `-- <cmd>` overrides commands for one run. |
|
|
58
|
-
| `monacori verify` | Run configured verification commands and store the log. Exits non-zero on failure. |
|
|
59
|
-
| `monacori diff` | Generate a browser-based side-by-side diff page. `--watch` serves a live-reloading review. |
|
|
37
|
+
| `mo` | Open the desktop diff-review app (alias for `monacori open`). |
|
|
60
38
|
| `monacori app` | Launch the desktop review app (same as `mo`). |
|
|
61
|
-
| `monacori
|
|
62
|
-
| `monacori
|
|
39
|
+
| `monacori init` | Initialize `.monacori/` in the current directory. |
|
|
40
|
+
| `monacori install` | Initialize and write agent instruction snippets. `--apply-agent-docs` patches `AGENTS.md` / `CLAUDE.md`. |
|
|
63
41
|
|
|
64
42
|
## Repository state
|
|
65
43
|
|
|
66
|
-
`monacori init` (run automatically by `mo`) creates
|
|
67
|
-
|
|
68
|
-
```text
|
|
69
|
-
.monacori/
|
|
70
|
-
config.json verification commands and diff defaults
|
|
71
|
-
state.md compact validation history
|
|
72
|
-
diffs/ generated browser diff reviews
|
|
73
|
-
reports/ validation reports
|
|
74
|
-
logs/ verification logs
|
|
75
|
-
```
|
|
76
|
-
|
|
77
|
-
Keep `.monacori/` ignored unless your team explicitly wants to commit validation state.
|
|
44
|
+
`monacori init` (run automatically by `mo`) creates a git-ignored `.monacori/` directory holding generated diff reviews and local config. Keep it ignored unless your team explicitly wants to commit review state.
|
|
78
45
|
|
|
79
46
|
## Design principles
|
|
80
47
|
|
|
81
|
-
-
|
|
82
|
-
-
|
|
48
|
+
- A real diff beats a chat log or a "done" claim.
|
|
49
|
+
- Review, comment, and hand-off live in one window — no copy-paste loop.
|
|
50
|
+
- Generated artifacts are plain static HTML and JSON.
|
|
83
51
|
- No required AI agent, terminal multiplexer, editor, or worktree strategy.
|
|
84
|
-
- The default command should be useful after any AI-generated edit.
|
|
85
|
-
- A change is not accepted until the evidence is clear or the gap is documented.
|
|
86
52
|
|
|
87
53
|
## License
|
|
88
54
|
|
package/dist/app-main.js
CHANGED
|
@@ -71,8 +71,37 @@ ipcMain.handle("monacori:pty-spawn", (_event, size) => {
|
|
|
71
71
|
if (mainWindow && !mainWindow.isDestroyed())
|
|
72
72
|
mainWindow.webContents.send(channel, payload);
|
|
73
73
|
};
|
|
74
|
-
|
|
75
|
-
|
|
74
|
+
// pty output can arrive as many tiny chunks (thousands/sec under fast output). One IPC per chunk floods
|
|
75
|
+
// the renderer, so coalesce: buffer chunks and flush on a short timer, or immediately once the buffer is
|
|
76
|
+
// large — bounding both IPC traffic and added latency.
|
|
77
|
+
let outBuf = "";
|
|
78
|
+
let flushTimer;
|
|
79
|
+
const flushOut = () => {
|
|
80
|
+
flushTimer = undefined;
|
|
81
|
+
if (!outBuf)
|
|
82
|
+
return;
|
|
83
|
+
const data = outBuf;
|
|
84
|
+
outBuf = "";
|
|
85
|
+
deliver("monacori:pty-data", { id, data });
|
|
86
|
+
};
|
|
87
|
+
t.onData((data) => {
|
|
88
|
+
outBuf += data;
|
|
89
|
+
if (outBuf.length >= 64 * 1024) {
|
|
90
|
+
if (flushTimer)
|
|
91
|
+
clearTimeout(flushTimer);
|
|
92
|
+
flushOut();
|
|
93
|
+
}
|
|
94
|
+
else if (!flushTimer) {
|
|
95
|
+
flushTimer = setTimeout(flushOut, 12);
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
t.onExit(() => {
|
|
99
|
+
if (flushTimer)
|
|
100
|
+
clearTimeout(flushTimer);
|
|
101
|
+
flushOut(); // deliver any buffered tail before signaling exit
|
|
102
|
+
terms.delete(id);
|
|
103
|
+
deliver("monacori:pty-exit", { id });
|
|
104
|
+
});
|
|
76
105
|
return { ok: true, id };
|
|
77
106
|
});
|
|
78
107
|
ipcMain.on("monacori:pty-write", (_event, msg) => { terms.get(msg?.id)?.write(msg.data); });
|
|
@@ -151,6 +180,8 @@ app.whenReady().then(async () => {
|
|
|
151
180
|
submenu: [
|
|
152
181
|
{ label: "All questions", accelerator: "Control+Command+Shift+/", click: () => sendMerged("q") },
|
|
153
182
|
{ label: "All change requests", accelerator: "Control+Command+Shift+.", click: () => sendMerged("c") },
|
|
183
|
+
// Cmd/Ctrl+Shift+N opens (and toggles) the single freeform prompt memo — a Markdown scratchpad.
|
|
184
|
+
{ label: "Prompt memo", accelerator: "CommandOrControl+Shift+N", click: () => mainWindow?.webContents.send("monacori:open-memo") },
|
|
154
185
|
{ type: "separator" },
|
|
155
186
|
// Whitespace-ignore re-runs git diff with --ignore-all-space and reloads (main-process action,
|
|
156
187
|
// so a menu checkbox is simpler than a renderer IPC round-trip).
|
|
@@ -190,7 +221,7 @@ app.whenReady().then(async () => {
|
|
|
190
221
|
{ type: "separator" },
|
|
191
222
|
{ label: "Focus Previous Pane", accelerator: "CommandOrControl+Alt+[", click: () => mainWindow?.webContents.send("monacori:terminal-pane-focus", -1) },
|
|
192
223
|
{ label: "Focus Next Pane", accelerator: "CommandOrControl+Alt+]", click: () => mainWindow?.webContents.send("monacori:terminal-pane-focus", 1) },
|
|
193
|
-
{ label: "Rename Pane", accelerator: "
|
|
224
|
+
{ label: "Rename Pane", accelerator: "CommandOrControl+Alt+R", click: () => mainWindow?.webContents.send("monacori:terminal-pane-rename") },
|
|
194
225
|
],
|
|
195
226
|
});
|
|
196
227
|
Menu.setApplicationMenu(Menu.buildFromTemplate(menuTemplate));
|
|
@@ -222,7 +253,10 @@ app.whenReady().then(async () => {
|
|
|
222
253
|
// paint and swap it in. The first build used to run synchronously *before* the window existed, so the
|
|
223
254
|
// screen stayed blank for the first few seconds of startup; now the user sees a loading screen instead.
|
|
224
255
|
await mainWindow.loadURL("data:text/html;charset=utf-8," + encodeURIComponent(LOADING_HTML));
|
|
225
|
-
|
|
256
|
+
// Give the loading spinner a few frames to actually paint before the (synchronous) first build blocks
|
|
257
|
+
// the main process — otherwise the spinner looks frozen until the build finishes. The boot overlay in
|
|
258
|
+
// the review HTML then takes over, so there's no blank gap when loadFile swaps the page in.
|
|
259
|
+
setTimeout(() => {
|
|
226
260
|
try {
|
|
227
261
|
const firstBuild = writeReviewFile(options);
|
|
228
262
|
currentSignature = firstBuild.signature;
|
|
@@ -235,7 +269,7 @@ app.whenReady().then(async () => {
|
|
|
235
269
|
console.error(error instanceof Error ? error.message : String(error));
|
|
236
270
|
app.quit();
|
|
237
271
|
}
|
|
238
|
-
});
|
|
272
|
+
}, 60);
|
|
239
273
|
}).catch((error) => {
|
|
240
274
|
console.error(error instanceof Error ? error.message : String(error));
|
|
241
275
|
app.quit();
|
|
@@ -260,7 +294,13 @@ async function refreshIfChanged() {
|
|
|
260
294
|
const next = writeReviewFile(options);
|
|
261
295
|
if (next.signature !== currentSignature) {
|
|
262
296
|
currentSignature = next.signature;
|
|
263
|
-
|
|
297
|
+
// Refresh the diff in place instead of reloading the window. A full reload re-runs the renderer,
|
|
298
|
+
// whose beforeunload kills every pty — so an integrated terminal running claude/codex would die on
|
|
299
|
+
// each working-tree change. We send only the compact update payload (diff/trees/status/data — no
|
|
300
|
+
// xterm blob), and the renderer transplants it + re-fetches per-file bodies/source over the existing
|
|
301
|
+
// IPC (currentBodies/currentSourceData were just refreshed by writeReviewFile above).
|
|
302
|
+
if (next.update)
|
|
303
|
+
mainWindow.webContents.send("monacori:diff-update", next.update);
|
|
264
304
|
}
|
|
265
305
|
}
|
|
266
306
|
catch (error) {
|
|
@@ -284,7 +324,7 @@ function writeReviewFile(input) {
|
|
|
284
324
|
writeFileSync(reviewPath(), build.html);
|
|
285
325
|
currentBodies = build.lazyBodies ?? [];
|
|
286
326
|
currentSourceData = build.lazySourceData ?? "[]";
|
|
287
|
-
return { signature: build.signature };
|
|
327
|
+
return { signature: build.signature, html: build.html, update: build.update };
|
|
288
328
|
}
|
|
289
329
|
function reviewPath() {
|
|
290
330
|
return join(options.root, FLOW_DIR, REVIEW_FILE);
|
package/dist/build.js
CHANGED
|
@@ -3,7 +3,7 @@ import { basename } from "node:path";
|
|
|
3
3
|
import { isGitRepository } from "./git.js";
|
|
4
4
|
import { collectHttpEnvironments, collectReviewFileStates, collectSourceFiles, parseUnifiedDiff, readUnifiedDiff } from "./diff.js";
|
|
5
5
|
import { renderDiff2Html } from "./highlight.js";
|
|
6
|
-
import { diffSubtitle, renderDiffHtml, renderNotGitRepoHtml, shouldLazyRender, splitDiffForLazy } from "./render.js";
|
|
6
|
+
import { diffSubtitle, renderDiffHtml, renderDiffTree, renderNotGitRepoHtml, renderReviewStatus, renderSourceTree, shouldLazyRender, splitDiffForLazy } from "./render.js";
|
|
7
7
|
export function buildDiffReview(input) {
|
|
8
8
|
if (!isGitRepository(process.cwd())) {
|
|
9
9
|
return {
|
|
@@ -65,6 +65,27 @@ export function buildDiffReview(input) {
|
|
|
65
65
|
signature,
|
|
66
66
|
generatedAt,
|
|
67
67
|
});
|
|
68
|
+
// Compact payload for in-place refresh: just the regions the renderer swaps on a watch change. Reuses
|
|
69
|
+
// the same fragment renderers as the full page, minus the heavy bits (xterm blob, embedded source).
|
|
70
|
+
const update = {
|
|
71
|
+
signature,
|
|
72
|
+
generatedAt,
|
|
73
|
+
diffContainer: diffSplit.container || '<div class="empty" data-i18n="diff.noDiff">No diff to review.</div>',
|
|
74
|
+
changesPanel: renderDiffTree(files),
|
|
75
|
+
filesTree: renderSourceTree(sourceFiles),
|
|
76
|
+
reviewStatus: renderReviewStatus({
|
|
77
|
+
files: files.length,
|
|
78
|
+
hunks,
|
|
79
|
+
embeddedFiles: sourceFiles.filter((file) => file.embedded).length,
|
|
80
|
+
sourceFileCount: sourceFiles.length,
|
|
81
|
+
ignoreWhitespace: input.ignoreWhitespace,
|
|
82
|
+
watch: input.watch,
|
|
83
|
+
generatedAt,
|
|
84
|
+
}),
|
|
85
|
+
fileStates,
|
|
86
|
+
sourceFilesMeta: lazyLoad ? sourceFiles.map((file) => ({ ...file, content: "", image: "" })) : sourceFiles,
|
|
87
|
+
httpEnvironments,
|
|
88
|
+
};
|
|
68
89
|
return {
|
|
69
90
|
html,
|
|
70
91
|
files: files.length,
|
|
@@ -73,5 +94,6 @@ export function buildDiffReview(input) {
|
|
|
73
94
|
generatedAt,
|
|
74
95
|
lazyBodies: diffSplit.bodies,
|
|
75
96
|
lazySourceData: lazyLoad ? JSON.stringify(sourceFiles) : undefined,
|
|
97
|
+
update,
|
|
76
98
|
};
|
|
77
99
|
}
|
package/dist/commands.js
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
-
import { basename, dirname, join
|
|
2
|
+
import { basename, dirname, join } from "node:path";
|
|
3
3
|
import { spawn, spawnSync } from "node:child_process";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { createRequire } from "node:module";
|
|
6
6
|
import { AGENT_SNIPPET_FILE, CONFIG_FILE, DECISIONS_FILE, FLOW_DIR, GITIGNORE_FILE, STATE_FILE } from "./constants.js";
|
|
7
|
-
import {
|
|
8
|
-
import { git
|
|
9
|
-
import { createDiffReview, serveDiffWatch } from "./server.js";
|
|
7
|
+
import { parsePositiveInteger, readOption } from "./util.js";
|
|
8
|
+
import { git } from "./git.js";
|
|
10
9
|
const nodeRequire = createRequire(import.meta.url);
|
|
11
10
|
export function main() {
|
|
12
11
|
const rawArgs = process.argv.slice(2);
|
|
@@ -27,16 +26,6 @@ export function main() {
|
|
|
27
26
|
case "install":
|
|
28
27
|
installFlow(args);
|
|
29
28
|
break;
|
|
30
|
-
case "check":
|
|
31
|
-
case "go":
|
|
32
|
-
runCheck(args);
|
|
33
|
-
break;
|
|
34
|
-
case "verify":
|
|
35
|
-
runVerification(args);
|
|
36
|
-
break;
|
|
37
|
-
case "diff":
|
|
38
|
-
renderDiffReview(args);
|
|
39
|
-
break;
|
|
40
29
|
case "app":
|
|
41
30
|
case "review":
|
|
42
31
|
launchReviewApp(args);
|
|
@@ -44,12 +33,6 @@ export function main() {
|
|
|
44
33
|
case "open":
|
|
45
34
|
openCurrentRepository(args);
|
|
46
35
|
break;
|
|
47
|
-
case "status":
|
|
48
|
-
printStatus();
|
|
49
|
-
break;
|
|
50
|
-
case "report":
|
|
51
|
-
recordReport(args);
|
|
52
|
-
break;
|
|
53
36
|
case "--help":
|
|
54
37
|
case "-h":
|
|
55
38
|
case "help":
|
|
@@ -94,7 +77,7 @@ function initFlow(args) {
|
|
|
94
77
|
if (ignored) {
|
|
95
78
|
console.log(`Updated ${GITIGNORE_FILE} to ignore ${FLOW_DIR}/ validation artifacts.`);
|
|
96
79
|
}
|
|
97
|
-
console.log("Next: run `
|
|
80
|
+
console.log("Next: run `mo` to open the diff review app.");
|
|
98
81
|
}
|
|
99
82
|
}
|
|
100
83
|
function installFlow(args) {
|
|
@@ -115,117 +98,6 @@ function installFlow(args) {
|
|
|
115
98
|
console.log(`Next: add ${FLOW_DIR}/${AGENT_SNIPPET_FILE} to your agent instructions if desired.`);
|
|
116
99
|
}
|
|
117
100
|
}
|
|
118
|
-
function runCheck(args) {
|
|
119
|
-
if (args.includes("--help") || args.includes("-h")) {
|
|
120
|
-
printCheckHelp();
|
|
121
|
-
return;
|
|
122
|
-
}
|
|
123
|
-
ensureWritableFlowState();
|
|
124
|
-
const config = loadConfig();
|
|
125
|
-
const separator = args.indexOf("--");
|
|
126
|
-
const commandArgs = separator >= 0 ? args.slice(separator + 1) : [];
|
|
127
|
-
const optionArgs = separator >= 0 ? args.slice(0, separator) : args;
|
|
128
|
-
const noVerify = optionArgs.includes("--no-verify");
|
|
129
|
-
const noDiff = optionArgs.includes("--no-diff");
|
|
130
|
-
const openInBrowser = optionArgs.includes("--open");
|
|
131
|
-
const includeUntracked = optionArgs.includes("--include-untracked") || config.diff.includeUntracked;
|
|
132
|
-
const staged = optionArgs.includes("--staged");
|
|
133
|
-
const base = readOption(optionArgs, "--base");
|
|
134
|
-
const contextValue = readOption(optionArgs, "--context");
|
|
135
|
-
const context = contextValue ? parsePositiveInteger(contextValue, "--context") : config.diff.context;
|
|
136
|
-
const verification = noVerify
|
|
137
|
-
? { commands: [], failed: false, skipped: true }
|
|
138
|
-
: executeVerification(commandArgs.join(" "));
|
|
139
|
-
let review;
|
|
140
|
-
if (!noDiff) {
|
|
141
|
-
review = createDiffReview({
|
|
142
|
-
base,
|
|
143
|
-
staged,
|
|
144
|
-
includeUntracked,
|
|
145
|
-
context,
|
|
146
|
-
output: join(process.cwd(), FLOW_DIR, "diffs", `${timestampForFile()}-check.html`),
|
|
147
|
-
title: "monacori validation diff",
|
|
148
|
-
});
|
|
149
|
-
if (openInBrowser) {
|
|
150
|
-
spawnSync("open", [review.path], { stdio: "ignore" });
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
const reportPath = writeCheckReport({ verification, review });
|
|
154
|
-
console.log("# monacori check");
|
|
155
|
-
console.log(`Verification: ${verification.skipped ? "skipped" : verification.failed ? "failed" : "passed"}`);
|
|
156
|
-
if (verification.logPath) {
|
|
157
|
-
console.log(`Log: ${relative(process.cwd(), verification.logPath)}`);
|
|
158
|
-
}
|
|
159
|
-
if (review) {
|
|
160
|
-
console.log(`Diff review: ${relative(process.cwd(), review.path)}`);
|
|
161
|
-
console.log(`Files: ${review.files}`);
|
|
162
|
-
console.log(`Hunks: ${review.hunks}`);
|
|
163
|
-
}
|
|
164
|
-
console.log(`Report: ${relative(process.cwd(), reportPath)}`);
|
|
165
|
-
if (verification.failed) {
|
|
166
|
-
process.exit(1);
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
function runVerification(args) {
|
|
170
|
-
const separator = args.indexOf("--");
|
|
171
|
-
const explicitCommand = separator >= 0 ? args.slice(separator + 1).join(" ") : "";
|
|
172
|
-
const result = executeVerification(explicitCommand, { requireCommands: true });
|
|
173
|
-
if (result.logPath) {
|
|
174
|
-
console.log(`Verification log: ${relative(process.cwd(), result.logPath)}`);
|
|
175
|
-
}
|
|
176
|
-
if (result.failed) {
|
|
177
|
-
console.error("Verification failed.");
|
|
178
|
-
process.exit(1);
|
|
179
|
-
}
|
|
180
|
-
console.log("Verification passed.");
|
|
181
|
-
}
|
|
182
|
-
function renderDiffReview(args) {
|
|
183
|
-
if (args.includes("--help") || args.includes("-h")) {
|
|
184
|
-
printDiffHelp();
|
|
185
|
-
return;
|
|
186
|
-
}
|
|
187
|
-
ensureWritableFlowState();
|
|
188
|
-
const config = loadConfig();
|
|
189
|
-
const contextValue = readOption(args, "--context");
|
|
190
|
-
const context = contextValue ? parsePositiveInteger(contextValue, "--context") : config.diff.context;
|
|
191
|
-
const base = readOption(args, "--base");
|
|
192
|
-
const staged = args.includes("--staged");
|
|
193
|
-
const includeUntracked = args.includes("--include-untracked") || config.diff.includeUntracked;
|
|
194
|
-
const openInBrowser = args.includes("--open");
|
|
195
|
-
const watch = args.includes("--watch");
|
|
196
|
-
const ignoreWhitespace = args.includes("--ignore-whitespace");
|
|
197
|
-
if (watch) {
|
|
198
|
-
serveDiffWatch({
|
|
199
|
-
base,
|
|
200
|
-
staged,
|
|
201
|
-
includeUntracked,
|
|
202
|
-
context,
|
|
203
|
-
openInBrowser,
|
|
204
|
-
port: readOption(args, "--port"),
|
|
205
|
-
ignoreWhitespace,
|
|
206
|
-
});
|
|
207
|
-
return;
|
|
208
|
-
}
|
|
209
|
-
const output = readOption(args, "--output") ??
|
|
210
|
-
join(process.cwd(), FLOW_DIR, "diffs", `${timestampForFile()}-review.html`);
|
|
211
|
-
const result = createDiffReview({
|
|
212
|
-
base,
|
|
213
|
-
staged,
|
|
214
|
-
includeUntracked,
|
|
215
|
-
context,
|
|
216
|
-
output,
|
|
217
|
-
title: "monacori diff review",
|
|
218
|
-
ignoreWhitespace,
|
|
219
|
-
});
|
|
220
|
-
if (openInBrowser) {
|
|
221
|
-
spawnSync("open", [result.path], { stdio: "ignore" });
|
|
222
|
-
}
|
|
223
|
-
console.log(`Diff review: ${relative(process.cwd(), result.path)}`);
|
|
224
|
-
console.log(`URL: ${result.url}`);
|
|
225
|
-
console.log(`Files: ${result.files}`);
|
|
226
|
-
console.log(`Hunks: ${result.hunks}`);
|
|
227
|
-
console.log("Keys: F7 next hunk, Shift+F7 previous hunk, Shift Shift search files, Cmd/Ctrl+E recent files, Cmd/Ctrl+Down jump to symbol.");
|
|
228
|
-
}
|
|
229
101
|
function launchReviewApp(args) {
|
|
230
102
|
if (args.includes("--help") || args.includes("-h")) {
|
|
231
103
|
printAppHelp();
|
|
@@ -290,138 +162,6 @@ function resolveElectronBinary() {
|
|
|
290
162
|
function appMainPath() {
|
|
291
163
|
return join(dirname(fileURLToPath(import.meta.url)), "app-main.js");
|
|
292
164
|
}
|
|
293
|
-
function printStatus() {
|
|
294
|
-
ensureInitialized();
|
|
295
|
-
const config = loadConfig();
|
|
296
|
-
const git = readGitSnapshot(process.cwd());
|
|
297
|
-
const reports = listRecentFiles(join(process.cwd(), FLOW_DIR, "reports"), 5);
|
|
298
|
-
const logs = listRecentFiles(join(process.cwd(), FLOW_DIR, "logs"), 5);
|
|
299
|
-
console.log(`# ${config.projectName} validation status`);
|
|
300
|
-
console.log("");
|
|
301
|
-
console.log(`Branch: ${git.branch || "(unknown)"}`);
|
|
302
|
-
console.log("");
|
|
303
|
-
console.log("## Git status");
|
|
304
|
-
console.log(git.status || "clean");
|
|
305
|
-
console.log("");
|
|
306
|
-
console.log("## Diff stat");
|
|
307
|
-
console.log(git.diffStat || "no diff");
|
|
308
|
-
console.log("");
|
|
309
|
-
console.log("## Verification commands");
|
|
310
|
-
const commands = getVerificationCommands(config);
|
|
311
|
-
if (commands.length === 0) {
|
|
312
|
-
console.log("none configured");
|
|
313
|
-
}
|
|
314
|
-
else {
|
|
315
|
-
for (const command of commands) {
|
|
316
|
-
console.log(`- ${command}`);
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
console.log("");
|
|
320
|
-
console.log("## Recent reports");
|
|
321
|
-
console.log(reports.length === 0 ? "none" : reports.map((path) => `- ${relative(process.cwd(), path)}`).join("\n"));
|
|
322
|
-
console.log("");
|
|
323
|
-
console.log("## Recent logs");
|
|
324
|
-
console.log(logs.length === 0 ? "none" : logs.map((path) => `- ${relative(process.cwd(), path)}`).join("\n"));
|
|
325
|
-
}
|
|
326
|
-
function recordReport(args) {
|
|
327
|
-
ensureWritableFlowState();
|
|
328
|
-
const file = readOption(args, "--file");
|
|
329
|
-
const label = readOption(args, "--label") ?? "manual";
|
|
330
|
-
const body = file ? readFileSync(file, "utf8") : readStdin();
|
|
331
|
-
if (body.trim().length === 0) {
|
|
332
|
-
throw new Error("No report content provided. Pass --file or pipe report text on stdin.");
|
|
333
|
-
}
|
|
334
|
-
const timestamp = timestampForFile();
|
|
335
|
-
const reportDir = join(process.cwd(), FLOW_DIR, "reports");
|
|
336
|
-
mkdirSync(reportDir, { recursive: true });
|
|
337
|
-
const reportPath = join(reportDir, `${timestamp}-${sanitizeFilePart(label)}.md`);
|
|
338
|
-
writeFileSync(reportPath, [
|
|
339
|
-
`# Monacori Report: ${label}`,
|
|
340
|
-
"",
|
|
341
|
-
`Recorded: ${new Date().toISOString()}`,
|
|
342
|
-
"",
|
|
343
|
-
body.trim(),
|
|
344
|
-
"",
|
|
345
|
-
].join("\n"));
|
|
346
|
-
appendToState(`\n## Report ${timestamp} (${label})\n\n${summarizeForState(body)}\n`);
|
|
347
|
-
console.log(`Recorded ${relative(process.cwd(), reportPath)}`);
|
|
348
|
-
}
|
|
349
|
-
function executeVerification(explicitCommand = "", options = {}) {
|
|
350
|
-
ensureWritableFlowState();
|
|
351
|
-
const config = loadConfig();
|
|
352
|
-
const commands = explicitCommand.trim() ? [explicitCommand.trim()] : getVerificationCommands(config);
|
|
353
|
-
if (commands.length === 0) {
|
|
354
|
-
if (options.requireCommands) {
|
|
355
|
-
throw new Error(`No verification commands found. Add them to ${FLOW_DIR}/${CONFIG_FILE} or pass \`-- <command>\`.`);
|
|
356
|
-
}
|
|
357
|
-
return { commands: [], failed: false, skipped: true };
|
|
358
|
-
}
|
|
359
|
-
const logPath = join(process.cwd(), FLOW_DIR, "logs", `verify-${timestampForFile()}.log`);
|
|
360
|
-
const chunks = [];
|
|
361
|
-
let failed = false;
|
|
362
|
-
for (const command of commands) {
|
|
363
|
-
chunks.push(`$ ${command}\n`);
|
|
364
|
-
const result = spawnSync(command, {
|
|
365
|
-
cwd: process.cwd(),
|
|
366
|
-
shell: true,
|
|
367
|
-
encoding: "utf8",
|
|
368
|
-
env: process.env,
|
|
369
|
-
maxBuffer: 1024 * 1024 * 100,
|
|
370
|
-
});
|
|
371
|
-
chunks.push(result.stdout ?? "");
|
|
372
|
-
chunks.push(result.stderr ?? "");
|
|
373
|
-
chunks.push(`\nexit: ${result.status ?? 1}\n\n`);
|
|
374
|
-
if ((result.status ?? 1) !== 0) {
|
|
375
|
-
failed = true;
|
|
376
|
-
break;
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
writeFileSync(logPath, chunks.join(""));
|
|
380
|
-
return { commands, failed, skipped: false, logPath };
|
|
381
|
-
}
|
|
382
|
-
function writeCheckReport(input) {
|
|
383
|
-
const timestamp = timestampForFile();
|
|
384
|
-
const git = readGitSnapshot(process.cwd());
|
|
385
|
-
const reportDir = join(process.cwd(), FLOW_DIR, "reports");
|
|
386
|
-
mkdirSync(reportDir, { recursive: true });
|
|
387
|
-
const reportPath = join(reportDir, `${timestamp}-check.md`);
|
|
388
|
-
const verificationStatus = input.verification.skipped
|
|
389
|
-
? "skipped"
|
|
390
|
-
: input.verification.failed
|
|
391
|
-
? "failed"
|
|
392
|
-
: "passed";
|
|
393
|
-
const report = [
|
|
394
|
-
"# Monacori Validation Check",
|
|
395
|
-
"",
|
|
396
|
-
`Recorded: ${new Date().toISOString()}`,
|
|
397
|
-
`Branch: ${git.branch || "(unknown)"}`,
|
|
398
|
-
`Verification: ${verificationStatus}`,
|
|
399
|
-
input.verification.logPath ? `Log: ${relative(process.cwd(), input.verification.logPath)}` : "",
|
|
400
|
-
input.review ? `Diff review: ${relative(process.cwd(), input.review.path)}` : "",
|
|
401
|
-
input.review ? `Changed files: ${input.review.files}` : "",
|
|
402
|
-
input.review ? `Changed hunks: ${input.review.hunks}` : "",
|
|
403
|
-
"",
|
|
404
|
-
"## Commands",
|
|
405
|
-
input.verification.commands.length === 0
|
|
406
|
-
? "- none"
|
|
407
|
-
: input.verification.commands.map((command) => `- \`${command}\``).join("\n"),
|
|
408
|
-
"",
|
|
409
|
-
"## Git Status",
|
|
410
|
-
codeBlock(git.status || "clean"),
|
|
411
|
-
"",
|
|
412
|
-
"## Diff Stat",
|
|
413
|
-
codeBlock(git.diffStat || "no diff"),
|
|
414
|
-
"",
|
|
415
|
-
].filter((line) => line !== "").join("\n");
|
|
416
|
-
writeFileSync(reportPath, report);
|
|
417
|
-
appendToState(`\n## Check ${timestamp}\n\n- Verification: ${verificationStatus}\n${input.review ? `- Diff review: ${relative(process.cwd(), input.review.path)}\n` : ""}`);
|
|
418
|
-
return reportPath;
|
|
419
|
-
}
|
|
420
|
-
function appendToState(content) {
|
|
421
|
-
const path = join(process.cwd(), FLOW_DIR, STATE_FILE);
|
|
422
|
-
const current = existsSync(path) ? readFileSync(path, "utf8") : "";
|
|
423
|
-
writeFileSync(path, `${current.trimEnd()}\n${content}`);
|
|
424
|
-
}
|
|
425
165
|
function initialState(config) {
|
|
426
166
|
return [
|
|
427
167
|
"# Monacori Validation State",
|
|
@@ -442,27 +182,24 @@ function initialDecisions() {
|
|
|
442
182
|
return [
|
|
443
183
|
"# Monacori Decisions",
|
|
444
184
|
"",
|
|
445
|
-
"Record durable
|
|
185
|
+
"Record durable review decisions here so they do not depend on chat memory.",
|
|
446
186
|
"",
|
|
447
187
|
].join("\n");
|
|
448
188
|
}
|
|
449
189
|
function agentSnippet() {
|
|
450
190
|
return [
|
|
451
191
|
"<!-- MONACORI:START -->",
|
|
452
|
-
"## monacori
|
|
192
|
+
"## monacori Diff Review",
|
|
453
193
|
"",
|
|
454
|
-
"This repository uses monacori to
|
|
194
|
+
"This repository uses monacori to help humans review AI-generated code changes side-by-side.",
|
|
455
195
|
"",
|
|
456
|
-
"
|
|
196
|
+
"After making code changes:",
|
|
457
197
|
"",
|
|
458
|
-
"-
|
|
459
|
-
"- Use `monacori app --include-untracked` while changes are still moving.",
|
|
198
|
+
"- The user can run `mo` to open the diff review app and inspect your changes.",
|
|
460
199
|
"- Inspect changed hunks with F7 / Shift+F7.",
|
|
461
200
|
"- Use Shift Shift in the diff review to search indexed files, including unchanged files.",
|
|
462
201
|
"- In source previews, use Cmd/Ctrl+Down to jump to the declaration-like match under the cursor.",
|
|
463
|
-
"-
|
|
464
|
-
"",
|
|
465
|
-
"Do not claim a change is done without verification evidence or a precise explanation of why verification could not run.",
|
|
202
|
+
"- Inline comments left in the review are bundled into a prompt and sent back to the session.",
|
|
466
203
|
"<!-- MONACORI:END -->",
|
|
467
204
|
"",
|
|
468
205
|
].join("\n");
|
|
@@ -508,9 +245,6 @@ function loadConfig() {
|
|
|
508
245
|
},
|
|
509
246
|
};
|
|
510
247
|
}
|
|
511
|
-
function getVerificationCommands(config) {
|
|
512
|
-
return config.verification.commands.filter((command) => command.trim().length > 0);
|
|
513
|
-
}
|
|
514
248
|
function writeIfMissing(path, content, force) {
|
|
515
249
|
if (!force && existsSync(path)) {
|
|
516
250
|
return;
|
|
@@ -582,26 +316,14 @@ function packageScriptCommand(manager, script) {
|
|
|
582
316
|
function printHelp() {
|
|
583
317
|
console.log(`monacori
|
|
584
318
|
|
|
585
|
-
|
|
319
|
+
Desktop review app for AI-generated code changes.
|
|
586
320
|
|
|
587
321
|
Usage:
|
|
588
322
|
mo
|
|
589
323
|
monacori open [--base HEAD] [--staged] [--tracked-only]
|
|
590
|
-
monacori
|
|
324
|
+
monacori app [--base HEAD] [--staged] [--include-untracked]
|
|
591
325
|
monacori init [--force]
|
|
592
326
|
monacori install [--force] [--apply-agent-docs]
|
|
593
|
-
monacori verify [-- <command>]
|
|
594
|
-
monacori diff [--base HEAD] [--staged] [--include-untracked] [--open] [--watch]
|
|
595
|
-
monacori app [--base HEAD] [--staged] [--include-untracked]
|
|
596
|
-
monacori review [--base HEAD] [--staged] [--include-untracked]
|
|
597
|
-
monacori status
|
|
598
|
-
monacori report [--label manual] [--file report.md]
|
|
599
|
-
|
|
600
|
-
Default loop:
|
|
601
|
-
1. Let an AI agent edit code.
|
|
602
|
-
2. Run: mo
|
|
603
|
-
3. Run: monacori check --include-untracked
|
|
604
|
-
4. Only accept the change when verification evidence is clear.
|
|
605
327
|
|
|
606
328
|
Diff review keys:
|
|
607
329
|
F7 next changed hunk
|
|
@@ -626,42 +348,6 @@ Options:
|
|
|
626
348
|
--tracked-only inspect tracked changes only
|
|
627
349
|
`);
|
|
628
350
|
}
|
|
629
|
-
function printCheckHelp() {
|
|
630
|
-
console.log(`monacori check
|
|
631
|
-
|
|
632
|
-
Run configured verification and create a reviewable diff artifact.
|
|
633
|
-
|
|
634
|
-
Usage:
|
|
635
|
-
monacori check [--include-untracked] [--staged] [--base HEAD] [--context 12] [--open] [--no-verify] [--no-diff] [-- <command>]
|
|
636
|
-
|
|
637
|
-
Examples:
|
|
638
|
-
monacori check --include-untracked --open
|
|
639
|
-
monacori check -- npm test
|
|
640
|
-
monacori check --no-verify --include-untracked
|
|
641
|
-
`);
|
|
642
|
-
}
|
|
643
|
-
function printDiffHelp() {
|
|
644
|
-
console.log(`monacori diff
|
|
645
|
-
|
|
646
|
-
Generate a browser-based side-by-side Git diff review.
|
|
647
|
-
|
|
648
|
-
Usage:
|
|
649
|
-
monacori diff [--base HEAD] [--staged] [--include-untracked] [--context 12] [--output review.html] [--open] [--watch] [--port 0]
|
|
650
|
-
|
|
651
|
-
Keys in the review page:
|
|
652
|
-
F7 next changed hunk
|
|
653
|
-
Shift+F7 previous changed hunk
|
|
654
|
-
] / [ fallback hunk navigation
|
|
655
|
-
Shift Shift search indexed files, including unchanged files
|
|
656
|
-
Cmd/Ctrl+E recent files
|
|
657
|
-
Cmd/Ctrl+Down jump to symbol under cursor
|
|
658
|
-
|
|
659
|
-
The sidebar groups changed files as a folder tree. Use Search to filter paths and indexed file contents.
|
|
660
|
-
The Files tab opens read-only source previews, including unchanged files when they fit the local review budget.
|
|
661
|
-
Viewed marks are tied to file signatures, so a changed file becomes unviewed again after reload.
|
|
662
|
-
Use --watch to serve a live review that reloads when the working tree changes.
|
|
663
|
-
`);
|
|
664
|
-
}
|
|
665
351
|
function printAppHelp() {
|
|
666
352
|
console.log(`monacori app
|
|
667
353
|
|