mono-pilot 0.2.9 → 0.2.12
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 +270 -7
- package/dist/src/agents-paths.js +36 -0
- package/dist/src/brief/blocks.js +83 -0
- package/dist/src/brief/defaults.js +60 -0
- package/dist/src/brief/frontmatter.js +53 -0
- package/dist/src/brief/paths.js +10 -0
- package/dist/src/brief/reflection.js +27 -0
- package/dist/src/cli.js +62 -5
- package/dist/src/cluster/bus.js +102 -0
- package/dist/src/cluster/follower.js +137 -0
- package/dist/src/cluster/init.js +182 -0
- package/dist/src/cluster/leader.js +97 -0
- package/dist/src/cluster/log.js +49 -0
- package/dist/src/cluster/protocol.js +34 -0
- package/dist/src/cluster/services/bus.js +243 -0
- package/dist/src/cluster/services/embedding.js +12 -0
- package/dist/src/cluster/socket.js +86 -0
- package/dist/src/cluster/test-bus.js +175 -0
- package/dist/src/cluster_v2/connection-lifecycle.js +31 -0
- package/dist/src/cluster_v2/connection-lifecycle.test.js +24 -0
- package/dist/src/cluster_v2/connection.js +159 -0
- package/dist/src/cluster_v2/connection.test.js +55 -0
- package/dist/src/cluster_v2/events.js +102 -0
- package/dist/src/cluster_v2/index.js +2 -0
- package/dist/src/cluster_v2/observability.js +99 -0
- package/dist/src/cluster_v2/observability.test.js +46 -0
- package/dist/src/cluster_v2/rpc.js +389 -0
- package/dist/src/cluster_v2/rpc.test.js +110 -0
- package/dist/src/cluster_v2/runtime.failover.integration.test.js +156 -0
- package/dist/src/cluster_v2/runtime.js +531 -0
- package/dist/src/cluster_v2/runtime.lease-compromise.integration.test.js +91 -0
- package/dist/src/cluster_v2/runtime.lifecycle.integration.test.js +225 -0
- package/dist/src/cluster_v2/services/bus.integration.test.js +140 -0
- package/dist/src/cluster_v2/services/bus.js +450 -0
- package/dist/src/cluster_v2/services/discord/auth-store.js +82 -0
- package/dist/src/cluster_v2/services/discord/collector.js +569 -0
- package/dist/src/cluster_v2/services/discord/index.js +1 -0
- package/dist/src/cluster_v2/services/discord/oauth.js +87 -0
- package/dist/src/cluster_v2/services/discord/rpc-client.js +325 -0
- package/dist/src/cluster_v2/services/embedding.js +66 -0
- package/dist/src/cluster_v2/services/registry-cache.js +107 -0
- package/dist/src/cluster_v2/services/registry-cache.test.js +66 -0
- package/dist/src/cluster_v2/services/registry.js +36 -0
- package/dist/src/cluster_v2/services/twitter/collector.js +1055 -0
- package/dist/src/cluster_v2/services/twitter/index.js +1 -0
- package/dist/src/config/digest.js +78 -0
- package/dist/src/config/discord.js +143 -0
- package/dist/src/config/image-gen.js +48 -0
- package/dist/src/config/mono-pilot.js +31 -0
- package/dist/src/config/twitter.js +100 -0
- package/dist/src/extensions/cluster.js +311 -0
- package/dist/src/extensions/commands/build-memory.js +76 -0
- package/dist/src/extensions/commands/digest/backfill.js +779 -0
- package/dist/src/extensions/commands/digest/index.js +1133 -0
- package/dist/src/extensions/commands/image-model.js +214 -0
- package/dist/src/extensions/game/bus-injection.js +47 -0
- package/dist/src/extensions/game/identity.js +83 -0
- package/dist/src/extensions/game/mailbox.js +61 -0
- package/dist/src/extensions/game/system-prompt.js +134 -0
- package/dist/src/extensions/game/tools.js +28 -0
- package/dist/src/extensions/lifecycle.js +337 -0
- package/dist/src/extensions/mode-runtime.js +26 -2
- package/dist/src/extensions/mono-game.js +66 -0
- package/dist/src/extensions/mono-pilot.js +100 -18
- package/dist/src/extensions/nvim.js +47 -0
- package/dist/src/extensions/session-hints.js +60 -35
- package/dist/src/extensions/sftp.js +897 -0
- package/dist/src/extensions/status.js +676 -0
- package/dist/src/extensions/system-events.js +478 -0
- package/dist/src/extensions/system-prompt.js +24 -14
- package/dist/src/extensions/user-message.js +94 -50
- package/dist/src/lsp/client.js +235 -0
- package/dist/src/lsp/index.js +165 -0
- package/dist/src/lsp/runtime.js +67 -0
- package/dist/src/lsp/server.js +242 -0
- package/dist/src/mcp/config.js +112 -0
- package/dist/src/{utils/mcp-client.js → mcp/protocol.js} +1 -100
- package/dist/src/mcp/servers.js +90 -0
- package/dist/src/memory/build-memory.js +103 -0
- package/dist/src/memory/config/defaults.js +55 -0
- package/dist/src/memory/config/loader.js +29 -0
- package/dist/src/memory/config/paths.js +9 -0
- package/dist/src/memory/config/resolve.js +90 -0
- package/dist/src/memory/config/types.js +1 -0
- package/dist/src/memory/embeddings/batch-runner.js +39 -0
- package/dist/src/memory/embeddings/cache.js +47 -0
- package/dist/src/memory/embeddings/chunk-limits.js +26 -0
- package/dist/src/memory/embeddings/input-limits.js +48 -0
- package/dist/src/memory/embeddings/local.js +108 -0
- package/dist/src/memory/embeddings/types.js +1 -0
- package/dist/src/memory/index-manager.js +552 -0
- package/dist/src/memory/indexing/embeddings.js +67 -0
- package/dist/src/memory/indexing/files.js +180 -0
- package/dist/src/memory/indexing/index-file.js +105 -0
- package/dist/src/memory/log.js +38 -0
- package/dist/src/memory/paths.js +15 -0
- package/dist/src/memory/runtime/index.js +299 -0
- package/dist/src/memory/runtime/thread.js +116 -0
- package/dist/src/memory/search/fts.js +57 -0
- package/dist/src/memory/search/hybrid.js +50 -0
- package/dist/src/memory/search/text.js +30 -0
- package/dist/src/memory/search/vector.js +43 -0
- package/dist/src/memory/session/content-hash.js +7 -0
- package/dist/src/memory/session/entry.js +33 -0
- package/dist/src/memory/session/flush-policy.js +34 -0
- package/dist/src/memory/session/hook.js +191 -0
- package/dist/src/memory/session/paths.js +15 -0
- package/dist/src/memory/session/session-reader.js +88 -0
- package/dist/src/memory/session/transcript/content-hash.js +7 -0
- package/dist/src/memory/session/transcript/entry.js +28 -0
- package/dist/src/memory/session/transcript/flush.js +56 -0
- package/dist/src/memory/session/transcript/paths.js +28 -0
- package/dist/src/memory/session/transcript/reader.js +112 -0
- package/dist/src/memory/session/transcript/state.js +31 -0
- package/dist/src/memory/store/schema.js +89 -0
- package/dist/src/memory/store/sqlite.js +89 -0
- package/dist/src/memory/types.js +1 -0
- package/dist/src/memory/warm.js +25 -0
- package/dist/src/rules/discovery.js +41 -0
- package/dist/{tools → src/tools}/README.md +29 -3
- package/dist/{tools → src/tools}/apply-patch-description.md +8 -2
- package/dist/{tools → src/tools}/apply-patch.js +174 -104
- package/dist/{tools → src/tools}/apply-patch.test.js +52 -1
- package/dist/{tools/ask-question.js → src/tools/ask-user-question.js} +3 -3
- package/dist/src/tools/ast-grep.js +357 -0
- package/dist/src/tools/brief-write.js +122 -0
- package/dist/src/tools/bus-send.js +100 -0
- package/dist/{tools → src/tools}/call-mcp-tool.js +40 -124
- package/dist/src/tools/codex-apply-patch-description.md +52 -0
- package/dist/src/tools/codex-apply-patch.js +540 -0
- package/dist/{tools → src/tools}/delete.js +24 -0
- package/dist/src/tools/exit-plan-mode.js +83 -0
- package/dist/{tools → src/tools}/fetch-mcp-resource.js +56 -100
- package/dist/src/tools/generate-image.js +567 -0
- package/dist/{tools → src/tools}/glob.js +55 -1
- package/dist/{tools → src/tools}/list-mcp-resources.js +46 -57
- package/dist/{tools → src/tools}/list-mcp-tools.js +52 -63
- package/dist/src/tools/ls.js +48 -0
- package/dist/src/tools/lsp-diagnostics.js +67 -0
- package/dist/src/tools/lsp-symbols.js +54 -0
- package/dist/src/tools/mailbox.js +85 -0
- package/dist/src/tools/memory-get.js +90 -0
- package/dist/src/tools/memory-search.js +180 -0
- package/dist/{tools → src/tools}/plan-mode-reminder.md +3 -4
- package/dist/{tools → src/tools}/read-file.js +8 -19
- package/dist/{tools → src/tools}/rg.js +10 -20
- package/dist/{tools → src/tools}/shell.js +19 -42
- package/dist/{tools → src/tools}/subagent.js +255 -6
- package/dist/{tools → src/tools}/switch-mode.js +37 -6
- package/dist/{tools → src/tools}/web-fetch.js +105 -7
- package/dist/{tools → src/tools}/web-search.js +29 -1
- package/package.json +21 -9
- /package/dist/{tools → src/tools}/ask-mode-reminder.md +0 -0
- /package/dist/{tools → src/tools}/rg.test.js +0 -0
- /package/dist/{tools → src/tools}/semantic-search-description.md +0 -0
- /package/dist/{tools → src/tools}/semantic-search.js +0 -0
- /package/dist/{tools → src/tools}/shell-description.md +0 -0
- /package/dist/{tools → src/tools}/subagent-description.md +0 -0
|
@@ -0,0 +1,540 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { dirname, isAbsolute, relative, resolve } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { keyHint } from "@mariozechner/pi-coding-agent";
|
|
6
|
+
import { Text } from "@mariozechner/pi-tui";
|
|
7
|
+
import { Type } from "@sinclair/typebox";
|
|
8
|
+
const BEGIN_PATCH_MARKER = "*** Begin Patch";
|
|
9
|
+
const END_PATCH_MARKER = "*** End Patch";
|
|
10
|
+
const ADD_FILE_MARKER = "*** Add File: ";
|
|
11
|
+
const DELETE_FILE_MARKER = "*** Delete File: ";
|
|
12
|
+
const UPDATE_FILE_MARKER = "*** Update File: ";
|
|
13
|
+
const MOVE_TO_MARKER = "*** Move to: ";
|
|
14
|
+
const EOF_MARKER = "*** End of File";
|
|
15
|
+
const CHANGE_CONTEXT_MARKER = "@@ ";
|
|
16
|
+
const EMPTY_CHANGE_CONTEXT_MARKER = "@@";
|
|
17
|
+
const DESCRIPTION = readFileSync(fileURLToPath(new URL("./codex-apply-patch-description.md", import.meta.url)), "utf-8").trim();
|
|
18
|
+
const codexApplyPatchSchema = Type.Object({
|
|
19
|
+
patch: Type.String({
|
|
20
|
+
description: "Patch document in codex apply_patch format. Supports multiple file hunks with Add/Delete/Update.",
|
|
21
|
+
}),
|
|
22
|
+
});
|
|
23
|
+
class ParseError extends Error {
|
|
24
|
+
lineNumber;
|
|
25
|
+
constructor(message, lineNumber) {
|
|
26
|
+
super(message);
|
|
27
|
+
this.name = "ParseError";
|
|
28
|
+
this.lineNumber = lineNumber;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function normalizeLineEndings(text) {
|
|
32
|
+
return text.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
33
|
+
}
|
|
34
|
+
function countPatchLines(patch) {
|
|
35
|
+
const normalized = normalizeLineEndings(patch).trim();
|
|
36
|
+
if (normalized.length === 0)
|
|
37
|
+
return 0;
|
|
38
|
+
return normalized.split("\n").length;
|
|
39
|
+
}
|
|
40
|
+
function ensureNotAborted(signal) {
|
|
41
|
+
if (signal?.aborted) {
|
|
42
|
+
throw new Error("Operation aborted");
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function hasStrictBoundaries(lines) {
|
|
46
|
+
if (lines.length < 2)
|
|
47
|
+
return false;
|
|
48
|
+
return lines[0]?.trim() === BEGIN_PATCH_MARKER && lines[lines.length - 1]?.trim() === END_PATCH_MARKER;
|
|
49
|
+
}
|
|
50
|
+
function parsePatchLinesWithLenientHeredoc(lines) {
|
|
51
|
+
if (hasStrictBoundaries(lines)) {
|
|
52
|
+
return lines;
|
|
53
|
+
}
|
|
54
|
+
const first = lines[0]?.trim();
|
|
55
|
+
const last = lines[lines.length - 1]?.trim();
|
|
56
|
+
const isHeredoc = (first === "<<EOF" || first === "<<'EOF'" || first === '<<"EOF"') && typeof last === "string" && last.endsWith("EOF");
|
|
57
|
+
if (!isHeredoc || lines.length < 4) {
|
|
58
|
+
if (lines[0]?.trim() !== BEGIN_PATCH_MARKER) {
|
|
59
|
+
throw new ParseError("The first line of the patch must be '*** Begin Patch'");
|
|
60
|
+
}
|
|
61
|
+
throw new ParseError("The last line of the patch must be '*** End Patch'");
|
|
62
|
+
}
|
|
63
|
+
const innerLines = lines.slice(1, -1);
|
|
64
|
+
if (!hasStrictBoundaries(innerLines)) {
|
|
65
|
+
if (innerLines[0]?.trim() !== BEGIN_PATCH_MARKER) {
|
|
66
|
+
throw new ParseError("The first line of the patch must be '*** Begin Patch'");
|
|
67
|
+
}
|
|
68
|
+
throw new ParseError("The last line of the patch must be '*** End Patch'");
|
|
69
|
+
}
|
|
70
|
+
return innerLines;
|
|
71
|
+
}
|
|
72
|
+
function parsePatch(patch) {
|
|
73
|
+
const normalized = normalizeLineEndings(patch);
|
|
74
|
+
const rawLines = normalized.trim().split("\n");
|
|
75
|
+
const lines = parsePatchLinesWithLenientHeredoc(rawLines);
|
|
76
|
+
const hunks = [];
|
|
77
|
+
let lineNumber = 2;
|
|
78
|
+
let remaining = lines.slice(1, -1);
|
|
79
|
+
while (remaining.length > 0) {
|
|
80
|
+
const parsed = parseOneHunk(remaining, lineNumber);
|
|
81
|
+
hunks.push(parsed.hunk);
|
|
82
|
+
lineNumber += parsed.consumedLines;
|
|
83
|
+
remaining = remaining.slice(parsed.consumedLines);
|
|
84
|
+
}
|
|
85
|
+
return {
|
|
86
|
+
patch: lines.join("\n"),
|
|
87
|
+
hunks,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
function parseOneHunk(lines, lineNumber) {
|
|
91
|
+
const firstLine = lines[0]?.trim() ?? "";
|
|
92
|
+
if (firstLine.startsWith(ADD_FILE_MARKER)) {
|
|
93
|
+
const path = firstLine.slice(ADD_FILE_MARKER.length);
|
|
94
|
+
let contents = "";
|
|
95
|
+
let consumed = 1;
|
|
96
|
+
for (const addLine of lines.slice(1)) {
|
|
97
|
+
if (!addLine.startsWith("+"))
|
|
98
|
+
break;
|
|
99
|
+
contents += `${addLine.slice(1)}\n`;
|
|
100
|
+
consumed += 1;
|
|
101
|
+
}
|
|
102
|
+
return {
|
|
103
|
+
hunk: { type: "add", path, contents },
|
|
104
|
+
consumedLines: consumed,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
if (firstLine.startsWith(DELETE_FILE_MARKER)) {
|
|
108
|
+
const path = firstLine.slice(DELETE_FILE_MARKER.length);
|
|
109
|
+
return {
|
|
110
|
+
hunk: { type: "delete", path },
|
|
111
|
+
consumedLines: 1,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
if (firstLine.startsWith(UPDATE_FILE_MARKER)) {
|
|
115
|
+
const path = firstLine.slice(UPDATE_FILE_MARKER.length);
|
|
116
|
+
let consumed = 1;
|
|
117
|
+
let remaining = lines.slice(1);
|
|
118
|
+
let movePath;
|
|
119
|
+
const moveLine = remaining[0];
|
|
120
|
+
if (typeof moveLine === "string" && moveLine.startsWith(MOVE_TO_MARKER)) {
|
|
121
|
+
movePath = moveLine.slice(MOVE_TO_MARKER.length);
|
|
122
|
+
consumed += 1;
|
|
123
|
+
remaining = remaining.slice(1);
|
|
124
|
+
}
|
|
125
|
+
const chunks = [];
|
|
126
|
+
while (remaining.length > 0) {
|
|
127
|
+
if (remaining[0]?.trim().length === 0) {
|
|
128
|
+
consumed += 1;
|
|
129
|
+
remaining = remaining.slice(1);
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
if (remaining[0]?.startsWith("***")) {
|
|
133
|
+
break;
|
|
134
|
+
}
|
|
135
|
+
const chunk = parseUpdateFileChunk(remaining, lineNumber + consumed, chunks.length === 0);
|
|
136
|
+
chunks.push(chunk.chunk);
|
|
137
|
+
consumed += chunk.consumedLines;
|
|
138
|
+
remaining = remaining.slice(chunk.consumedLines);
|
|
139
|
+
}
|
|
140
|
+
if (chunks.length === 0) {
|
|
141
|
+
throw new ParseError(`Update file hunk for path '${path}' is empty`, lineNumber);
|
|
142
|
+
}
|
|
143
|
+
return {
|
|
144
|
+
hunk: { type: "update", path, movePath, chunks },
|
|
145
|
+
consumedLines: consumed,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
throw new ParseError(`'${firstLine}' is not a valid hunk header. Valid hunk headers: '*** Add File: {path}', '*** Delete File: {path}', '*** Update File: {path}'`, lineNumber);
|
|
149
|
+
}
|
|
150
|
+
function parseUpdateFileChunk(lines, lineNumber, allowMissingContext) {
|
|
151
|
+
if (lines.length === 0) {
|
|
152
|
+
throw new ParseError("Update hunk does not contain any lines", lineNumber);
|
|
153
|
+
}
|
|
154
|
+
let changeContext;
|
|
155
|
+
let startIndex = 0;
|
|
156
|
+
if (lines[0] === EMPTY_CHANGE_CONTEXT_MARKER) {
|
|
157
|
+
startIndex = 1;
|
|
158
|
+
}
|
|
159
|
+
else if (lines[0]?.startsWith(CHANGE_CONTEXT_MARKER)) {
|
|
160
|
+
changeContext = lines[0].slice(CHANGE_CONTEXT_MARKER.length);
|
|
161
|
+
startIndex = 1;
|
|
162
|
+
}
|
|
163
|
+
else if (!allowMissingContext) {
|
|
164
|
+
throw new ParseError(`Expected update hunk to start with a @@ context marker, got: '${lines[0] ?? ""}'`, lineNumber);
|
|
165
|
+
}
|
|
166
|
+
if (startIndex >= lines.length) {
|
|
167
|
+
throw new ParseError("Update hunk does not contain any lines", lineNumber + 1);
|
|
168
|
+
}
|
|
169
|
+
const chunk = {
|
|
170
|
+
changeContext,
|
|
171
|
+
oldLines: [],
|
|
172
|
+
newLines: [],
|
|
173
|
+
isEndOfFile: false,
|
|
174
|
+
};
|
|
175
|
+
let parsedLines = 0;
|
|
176
|
+
for (const line of lines.slice(startIndex)) {
|
|
177
|
+
if (line === EOF_MARKER) {
|
|
178
|
+
if (parsedLines === 0) {
|
|
179
|
+
throw new ParseError("Update hunk does not contain any lines", lineNumber + 1);
|
|
180
|
+
}
|
|
181
|
+
chunk.isEndOfFile = true;
|
|
182
|
+
parsedLines += 1;
|
|
183
|
+
break;
|
|
184
|
+
}
|
|
185
|
+
const marker = line[0];
|
|
186
|
+
if (marker === undefined) {
|
|
187
|
+
chunk.oldLines.push("");
|
|
188
|
+
chunk.newLines.push("");
|
|
189
|
+
parsedLines += 1;
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
if (marker === " ") {
|
|
193
|
+
const text = line.slice(1);
|
|
194
|
+
chunk.oldLines.push(text);
|
|
195
|
+
chunk.newLines.push(text);
|
|
196
|
+
parsedLines += 1;
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
if (marker === "+") {
|
|
200
|
+
chunk.newLines.push(line.slice(1));
|
|
201
|
+
parsedLines += 1;
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
if (marker === "-") {
|
|
205
|
+
chunk.oldLines.push(line.slice(1));
|
|
206
|
+
parsedLines += 1;
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
if (parsedLines === 0) {
|
|
210
|
+
throw new ParseError(`Unexpected line found in update hunk: '${line}'. Every line should start with ' ' (context line), '+' (added line), or '-' (removed line)`, lineNumber + 1);
|
|
211
|
+
}
|
|
212
|
+
break;
|
|
213
|
+
}
|
|
214
|
+
return {
|
|
215
|
+
chunk,
|
|
216
|
+
consumedLines: parsedLines + startIndex,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
function resolveRelativePatchPath(cwd, patchPath) {
|
|
220
|
+
const trimmed = patchPath.trim();
|
|
221
|
+
if (trimmed.length === 0) {
|
|
222
|
+
throw new Error("Patch path cannot be empty.");
|
|
223
|
+
}
|
|
224
|
+
if (isAbsolute(trimmed)) {
|
|
225
|
+
throw new Error(`Patch path must be relative: ${patchPath}`);
|
|
226
|
+
}
|
|
227
|
+
return resolve(cwd, trimmed);
|
|
228
|
+
}
|
|
229
|
+
function seekSequence(lines, pattern, start, eof) {
|
|
230
|
+
if (pattern.length === 0) {
|
|
231
|
+
return start;
|
|
232
|
+
}
|
|
233
|
+
if (pattern.length > lines.length) {
|
|
234
|
+
return undefined;
|
|
235
|
+
}
|
|
236
|
+
const searchStart = eof && lines.length >= pattern.length ? lines.length - pattern.length : start;
|
|
237
|
+
const end = lines.length - pattern.length;
|
|
238
|
+
for (let i = searchStart; i <= end; i++) {
|
|
239
|
+
let match = true;
|
|
240
|
+
for (let j = 0; j < pattern.length; j++) {
|
|
241
|
+
if (lines[i + j] !== pattern[j]) {
|
|
242
|
+
match = false;
|
|
243
|
+
break;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
if (match)
|
|
247
|
+
return i;
|
|
248
|
+
}
|
|
249
|
+
for (let i = searchStart; i <= end; i++) {
|
|
250
|
+
let match = true;
|
|
251
|
+
for (let j = 0; j < pattern.length; j++) {
|
|
252
|
+
if (lines[i + j]?.trimEnd() !== pattern[j]?.trimEnd()) {
|
|
253
|
+
match = false;
|
|
254
|
+
break;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
if (match)
|
|
258
|
+
return i;
|
|
259
|
+
}
|
|
260
|
+
for (let i = searchStart; i <= end; i++) {
|
|
261
|
+
let match = true;
|
|
262
|
+
for (let j = 0; j < pattern.length; j++) {
|
|
263
|
+
if (lines[i + j]?.trim() !== pattern[j]?.trim()) {
|
|
264
|
+
match = false;
|
|
265
|
+
break;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
if (match)
|
|
269
|
+
return i;
|
|
270
|
+
}
|
|
271
|
+
const normalizeForFuzzyMatch = (value) => {
|
|
272
|
+
const source = value.trim();
|
|
273
|
+
let out = "";
|
|
274
|
+
for (const char of source) {
|
|
275
|
+
switch (char) {
|
|
276
|
+
case "\u2010":
|
|
277
|
+
case "\u2011":
|
|
278
|
+
case "\u2012":
|
|
279
|
+
case "\u2013":
|
|
280
|
+
case "\u2014":
|
|
281
|
+
case "\u2015":
|
|
282
|
+
case "\u2212":
|
|
283
|
+
out += "-";
|
|
284
|
+
break;
|
|
285
|
+
case "\u2018":
|
|
286
|
+
case "\u2019":
|
|
287
|
+
case "\u201A":
|
|
288
|
+
case "\u201B":
|
|
289
|
+
out += "'";
|
|
290
|
+
break;
|
|
291
|
+
case "\u201C":
|
|
292
|
+
case "\u201D":
|
|
293
|
+
case "\u201E":
|
|
294
|
+
case "\u201F":
|
|
295
|
+
out += '"';
|
|
296
|
+
break;
|
|
297
|
+
case "\u00A0":
|
|
298
|
+
case "\u2002":
|
|
299
|
+
case "\u2003":
|
|
300
|
+
case "\u2004":
|
|
301
|
+
case "\u2005":
|
|
302
|
+
case "\u2006":
|
|
303
|
+
case "\u2007":
|
|
304
|
+
case "\u2008":
|
|
305
|
+
case "\u2009":
|
|
306
|
+
case "\u200A":
|
|
307
|
+
case "\u202F":
|
|
308
|
+
case "\u205F":
|
|
309
|
+
case "\u3000":
|
|
310
|
+
out += " ";
|
|
311
|
+
break;
|
|
312
|
+
default:
|
|
313
|
+
out += char;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
return out;
|
|
317
|
+
};
|
|
318
|
+
for (let i = searchStart; i <= end; i++) {
|
|
319
|
+
let match = true;
|
|
320
|
+
for (let j = 0; j < pattern.length; j++) {
|
|
321
|
+
if (normalizeForFuzzyMatch(lines[i + j] ?? "") !== normalizeForFuzzyMatch(pattern[j] ?? "")) {
|
|
322
|
+
match = false;
|
|
323
|
+
break;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
if (match)
|
|
327
|
+
return i;
|
|
328
|
+
}
|
|
329
|
+
return undefined;
|
|
330
|
+
}
|
|
331
|
+
function applyReplacements(lines, replacements) {
|
|
332
|
+
for (const [startIndex, oldLength, newSegment] of replacements.slice().reverse()) {
|
|
333
|
+
lines.splice(startIndex, oldLength, ...newSegment);
|
|
334
|
+
}
|
|
335
|
+
return lines;
|
|
336
|
+
}
|
|
337
|
+
function computeReplacements(path, originalLines, chunks) {
|
|
338
|
+
const replacements = [];
|
|
339
|
+
let lineIndex = 0;
|
|
340
|
+
for (const chunk of chunks) {
|
|
341
|
+
if (chunk.changeContext) {
|
|
342
|
+
const contextIndex = seekSequence(originalLines, [chunk.changeContext], lineIndex, false);
|
|
343
|
+
if (contextIndex === undefined) {
|
|
344
|
+
throw new Error(`Failed to find context '${chunk.changeContext}' in ${path}`);
|
|
345
|
+
}
|
|
346
|
+
lineIndex = contextIndex + 1;
|
|
347
|
+
}
|
|
348
|
+
if (chunk.oldLines.length === 0) {
|
|
349
|
+
const insertionIndex = originalLines.at(-1) === "" ? originalLines.length - 1 : originalLines.length;
|
|
350
|
+
replacements.push([insertionIndex, 0, [...chunk.newLines]]);
|
|
351
|
+
continue;
|
|
352
|
+
}
|
|
353
|
+
let pattern = chunk.oldLines;
|
|
354
|
+
let nextSegment = chunk.newLines;
|
|
355
|
+
let found = seekSequence(originalLines, pattern, lineIndex, chunk.isEndOfFile);
|
|
356
|
+
if (found === undefined && pattern.at(-1) === "") {
|
|
357
|
+
pattern = pattern.slice(0, -1);
|
|
358
|
+
if (nextSegment.at(-1) === "") {
|
|
359
|
+
nextSegment = nextSegment.slice(0, -1);
|
|
360
|
+
}
|
|
361
|
+
found = seekSequence(originalLines, pattern, lineIndex, chunk.isEndOfFile);
|
|
362
|
+
}
|
|
363
|
+
if (found === undefined) {
|
|
364
|
+
throw new Error(`Failed to find expected lines in ${path}:\n${chunk.oldLines.join("\n")}`);
|
|
365
|
+
}
|
|
366
|
+
replacements.push([found, pattern.length, [...nextSegment]]);
|
|
367
|
+
lineIndex = found + pattern.length;
|
|
368
|
+
}
|
|
369
|
+
replacements.sort((a, b) => a[0] - b[0]);
|
|
370
|
+
return replacements;
|
|
371
|
+
}
|
|
372
|
+
async function deriveUpdatedFileContents(path, chunks) {
|
|
373
|
+
const originalContents = await readFile(path, "utf-8");
|
|
374
|
+
const originalLines = normalizeLineEndings(originalContents).split("\n");
|
|
375
|
+
if (originalLines.at(-1) === "") {
|
|
376
|
+
originalLines.pop();
|
|
377
|
+
}
|
|
378
|
+
const replacements = computeReplacements(path, originalLines, chunks);
|
|
379
|
+
const newLines = applyReplacements([...originalLines], replacements);
|
|
380
|
+
if (newLines.at(-1) !== "") {
|
|
381
|
+
newLines.push("");
|
|
382
|
+
}
|
|
383
|
+
return newLines.join("\n");
|
|
384
|
+
}
|
|
385
|
+
function formatDisplayPath(cwd, absolutePath) {
|
|
386
|
+
const rel = relative(cwd, absolutePath);
|
|
387
|
+
if (rel.length === 0)
|
|
388
|
+
return ".";
|
|
389
|
+
if (!rel.startsWith("..") && !isAbsolute(rel))
|
|
390
|
+
return rel;
|
|
391
|
+
return absolutePath;
|
|
392
|
+
}
|
|
393
|
+
function buildSummary(added, modified, deleted) {
|
|
394
|
+
const lines = ["Success. Updated the following files:"];
|
|
395
|
+
for (const path of added)
|
|
396
|
+
lines.push(`A ${path}`);
|
|
397
|
+
for (const path of modified)
|
|
398
|
+
lines.push(`M ${path}`);
|
|
399
|
+
for (const path of deleted)
|
|
400
|
+
lines.push(`D ${path}`);
|
|
401
|
+
return `${lines.join("\n")}\n`;
|
|
402
|
+
}
|
|
403
|
+
function buildCallSummary(patch) {
|
|
404
|
+
const lineCount = countPatchLines(patch);
|
|
405
|
+
const normalized = normalizeLineEndings(patch);
|
|
406
|
+
const firstOp = normalized
|
|
407
|
+
.split("\n")
|
|
408
|
+
.find((line) => line.startsWith(ADD_FILE_MARKER) || line.startsWith(DELETE_FILE_MARKER) || line.startsWith(UPDATE_FILE_MARKER));
|
|
409
|
+
if (!firstOp)
|
|
410
|
+
return `${lineCount} line(s)`;
|
|
411
|
+
if (firstOp.startsWith(ADD_FILE_MARKER)) {
|
|
412
|
+
return `add ${firstOp.slice(ADD_FILE_MARKER.length).trim()} (${lineCount} line(s))`;
|
|
413
|
+
}
|
|
414
|
+
if (firstOp.startsWith(DELETE_FILE_MARKER)) {
|
|
415
|
+
return `delete ${firstOp.slice(DELETE_FILE_MARKER.length).trim()} (${lineCount} line(s))`;
|
|
416
|
+
}
|
|
417
|
+
return `update ${firstOp.slice(UPDATE_FILE_MARKER.length).trim()} (${lineCount} line(s))`;
|
|
418
|
+
}
|
|
419
|
+
async function applyCodexPatchToFilesystem(options) {
|
|
420
|
+
const parsed = parsePatch(options.patchText);
|
|
421
|
+
if (parsed.hunks.length === 0) {
|
|
422
|
+
throw new Error("No files were modified.");
|
|
423
|
+
}
|
|
424
|
+
const added = [];
|
|
425
|
+
const modified = [];
|
|
426
|
+
const deleted = [];
|
|
427
|
+
for (const hunk of parsed.hunks) {
|
|
428
|
+
ensureNotAborted(options.signal);
|
|
429
|
+
if (hunk.type === "add") {
|
|
430
|
+
const path = resolveRelativePatchPath(options.cwd, hunk.path);
|
|
431
|
+
const parent = dirname(path);
|
|
432
|
+
if (parent && parent !== ".") {
|
|
433
|
+
await mkdir(parent, { recursive: true });
|
|
434
|
+
}
|
|
435
|
+
await writeFile(path, hunk.contents, "utf-8");
|
|
436
|
+
added.push(formatDisplayPath(options.cwd, path));
|
|
437
|
+
continue;
|
|
438
|
+
}
|
|
439
|
+
if (hunk.type === "delete") {
|
|
440
|
+
const path = resolveRelativePatchPath(options.cwd, hunk.path);
|
|
441
|
+
await rm(path);
|
|
442
|
+
deleted.push(formatDisplayPath(options.cwd, path));
|
|
443
|
+
continue;
|
|
444
|
+
}
|
|
445
|
+
const sourcePath = resolveRelativePatchPath(options.cwd, hunk.path);
|
|
446
|
+
const updatedContent = await deriveUpdatedFileContents(sourcePath, hunk.chunks);
|
|
447
|
+
if (hunk.movePath) {
|
|
448
|
+
const destinationPath = resolveRelativePatchPath(options.cwd, hunk.movePath);
|
|
449
|
+
const parent = dirname(destinationPath);
|
|
450
|
+
if (parent && parent !== ".") {
|
|
451
|
+
await mkdir(parent, { recursive: true });
|
|
452
|
+
}
|
|
453
|
+
await writeFile(destinationPath, updatedContent, "utf-8");
|
|
454
|
+
await rm(sourcePath);
|
|
455
|
+
modified.push(formatDisplayPath(options.cwd, destinationPath));
|
|
456
|
+
}
|
|
457
|
+
else {
|
|
458
|
+
await writeFile(sourcePath, updatedContent, "utf-8");
|
|
459
|
+
modified.push(formatDisplayPath(options.cwd, sourcePath));
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
const details = {
|
|
463
|
+
cwd: options.cwd,
|
|
464
|
+
patch_line_count: countPatchLines(parsed.patch),
|
|
465
|
+
hunk_count: parsed.hunks.length,
|
|
466
|
+
added,
|
|
467
|
+
modified,
|
|
468
|
+
deleted,
|
|
469
|
+
patch_text: parsed.patch,
|
|
470
|
+
};
|
|
471
|
+
return {
|
|
472
|
+
summary: buildSummary(added, modified, deleted),
|
|
473
|
+
details,
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
export default function codexApplyPatchExtension(pi) {
|
|
477
|
+
pi.registerTool({
|
|
478
|
+
name: "CodexApplyPatch",
|
|
479
|
+
label: "Codex Apply Patch",
|
|
480
|
+
description: DESCRIPTION,
|
|
481
|
+
parameters: codexApplyPatchSchema,
|
|
482
|
+
renderCall(args, theme) {
|
|
483
|
+
const input = args;
|
|
484
|
+
const summary = typeof input.patch === "string" ? buildCallSummary(input.patch) : "(missing patch)";
|
|
485
|
+
let text = theme.fg("toolTitle", theme.bold("CodexApplyPatch"));
|
|
486
|
+
text += ` ${theme.fg("toolOutput", summary)}`;
|
|
487
|
+
return new Text(text, 0, 0);
|
|
488
|
+
},
|
|
489
|
+
renderResult(result, { expanded, isPartial }, theme) {
|
|
490
|
+
if (isPartial) {
|
|
491
|
+
return new Text(theme.fg("muted", "Applying codex patch..."), 0, 0);
|
|
492
|
+
}
|
|
493
|
+
const textBlock = result.content.find((entry) => entry.type === "text" && typeof entry.text === "string");
|
|
494
|
+
if (!textBlock) {
|
|
495
|
+
return new Text(theme.fg("error", "No output."), 0, 0);
|
|
496
|
+
}
|
|
497
|
+
const fullText = textBlock.text;
|
|
498
|
+
const lineCount = fullText.split("\n").filter((line) => line.trim().length > 0).length;
|
|
499
|
+
if (!expanded) {
|
|
500
|
+
return new Text(theme.fg("muted", `${lineCount} lines (click or ${keyHint("expandTools", "to expand")})`), 0, 0);
|
|
501
|
+
}
|
|
502
|
+
const details = result.details;
|
|
503
|
+
let text = fullText
|
|
504
|
+
.split("\n")
|
|
505
|
+
.map((line) => theme.fg("toolOutput", line))
|
|
506
|
+
.join("\n");
|
|
507
|
+
if (details?.patch_text) {
|
|
508
|
+
const patchLines = details.patch_text
|
|
509
|
+
.split("\n")
|
|
510
|
+
.map((line) => theme.fg("toolOutput", line))
|
|
511
|
+
.join("\n");
|
|
512
|
+
text += `\n${theme.fg("toolOutput", "patch:")}\n${patchLines}`;
|
|
513
|
+
}
|
|
514
|
+
text += theme.fg("muted", `\n(click or ${keyHint("expandTools", "to collapse")})`);
|
|
515
|
+
return new Text(text, 0, 0);
|
|
516
|
+
},
|
|
517
|
+
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
518
|
+
try {
|
|
519
|
+
const { summary, details } = await applyCodexPatchToFilesystem({
|
|
520
|
+
patchText: params.patch,
|
|
521
|
+
cwd: ctx.cwd,
|
|
522
|
+
signal,
|
|
523
|
+
});
|
|
524
|
+
return {
|
|
525
|
+
content: [{ type: "text", text: summary }],
|
|
526
|
+
details,
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
catch (error) {
|
|
530
|
+
if (error instanceof ParseError) {
|
|
531
|
+
if (error.lineNumber !== undefined) {
|
|
532
|
+
throw new Error(`Invalid patch hunk on line ${error.lineNumber}: ${error.message}`);
|
|
533
|
+
}
|
|
534
|
+
throw new Error(`Invalid patch: ${error.message}`);
|
|
535
|
+
}
|
|
536
|
+
throw error;
|
|
537
|
+
}
|
|
538
|
+
},
|
|
539
|
+
});
|
|
540
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { stat, unlink } from "node:fs/promises";
|
|
2
2
|
import { isAbsolute, relative, resolve } from "node:path";
|
|
3
|
+
import { Text } from "@mariozechner/pi-tui";
|
|
3
4
|
import { Type } from "@sinclair/typebox";
|
|
4
5
|
// Tool docs are surfaced via system-prompt extension functions namespace.
|
|
5
6
|
const deleteSchema = Type.Object({
|
|
@@ -30,6 +31,29 @@ export default function (pi) {
|
|
|
30
31
|
label: "Delete",
|
|
31
32
|
description: "Delete file at specified path relative to workspace root; fails gracefully if file doesn't exist, security rejection, or undeletable.",
|
|
32
33
|
parameters: deleteSchema,
|
|
34
|
+
renderCall(args, theme) {
|
|
35
|
+
const input = args;
|
|
36
|
+
const filePath = typeof input.path === "string" && input.path.trim().length > 0
|
|
37
|
+
? input.path
|
|
38
|
+
: "(missing path)";
|
|
39
|
+
let text = theme.fg("toolTitle", theme.bold("Delete"));
|
|
40
|
+
text += ` ${theme.fg("toolOutput", filePath)}`;
|
|
41
|
+
return new Text(text, 0, 0);
|
|
42
|
+
},
|
|
43
|
+
renderResult(result, { isPartial }, theme) {
|
|
44
|
+
if (isPartial) {
|
|
45
|
+
return new Text(theme.fg("muted", "Deleting..."), 0, 0);
|
|
46
|
+
}
|
|
47
|
+
const details = result.details;
|
|
48
|
+
const status = details?.status ?? "error";
|
|
49
|
+
const resolvedPath = details?.resolved_path ?? details?.requested_path ?? "";
|
|
50
|
+
const reason = details?.reason;
|
|
51
|
+
if (status === "deleted") {
|
|
52
|
+
return new Text(theme.fg("toolOutput", `Deleted ${resolvedPath}`), 0, 0);
|
|
53
|
+
}
|
|
54
|
+
const message = reason ?? `${status}: ${resolvedPath}`;
|
|
55
|
+
return new Text(theme.fg("error", message), 0, 0);
|
|
56
|
+
},
|
|
33
57
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
34
58
|
const requestedPath = params.path;
|
|
35
59
|
let resolvedPath;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { Type } from "@sinclair/typebox";
|
|
2
|
+
import { createModeStateData, modeRuntimeStore, MODE_STATE_ENTRY_TYPE, } from "../extensions/mode-runtime.js";
|
|
3
|
+
const MODE_STATUS_KEY = "mono-pilot-mode";
|
|
4
|
+
const DESCRIPTION = `
|
|
5
|
+
Use this tool when you are in Plan mode, have finished updating the plan file, and are ready to leave Plan mode.
|
|
6
|
+
|
|
7
|
+
## How This Tool Works
|
|
8
|
+
- You should have already written your plan to the plan file specified in the plan mode system message
|
|
9
|
+
- This tool does NOT take plan content as input
|
|
10
|
+
- This tool switches the runtime mode from Plan to Agent
|
|
11
|
+
- This tool should be called only after the user explicitly approves the plan or asks to proceed with execution
|
|
12
|
+
|
|
13
|
+
## When to Use This Tool
|
|
14
|
+
IMPORTANT: Only use this tool when the task requires planning implementation steps for code changes. For research-only tasks where you're gathering information, searching files, or reading files, do NOT use this tool.
|
|
15
|
+
|
|
16
|
+
## Before Using This Tool
|
|
17
|
+
Ensure your plan is complete and unambiguous:
|
|
18
|
+
- If you have unresolved questions about requirements or approach, use AskQuestion first
|
|
19
|
+
- If you do not see a plan file path in system_reminder, do NOT call this tool because there is no concrete plan artifact to finalize and no need to exit Plan mode for execution.
|
|
20
|
+
- Present the finalized plan to the user and wait for explicit approval before calling this tool
|
|
21
|
+
- Once your plan is finalized, use THIS tool to exit Plan mode
|
|
22
|
+
|
|
23
|
+
**Important:** Do NOT call this tool immediately after drafting a plan. Wait for explicit user approval in the conversation first.
|
|
24
|
+
|
|
25
|
+
## Examples
|
|
26
|
+
|
|
27
|
+
1. Initial task: "Search for and understand the implementation of vim mode in the codebase" - Do not use the exit plan mode tool because you are not planning the implementation steps of a task.
|
|
28
|
+
2. Initial task: "Help me implement yank mode for vim" - Draft and present the plan, wait for user approval, then use this tool.
|
|
29
|
+
3. Initial task: "Add a new feature to handle user authentication" - If unsure about auth method (OAuth, JWT, etc.), use AskQuestion first, present the finalized plan, then call this tool only after user approval.
|
|
30
|
+
`.trim();
|
|
31
|
+
const exitPlanModeSchema = Type.Object({});
|
|
32
|
+
function updateModeStatus(ctx) {
|
|
33
|
+
if (!ctx.hasUI)
|
|
34
|
+
return;
|
|
35
|
+
const { activeMode } = modeRuntimeStore.getSnapshot();
|
|
36
|
+
const statusText = activeMode === "plan"
|
|
37
|
+
? ctx.ui.theme.fg("warning", "mode:plan")
|
|
38
|
+
: activeMode === "ask"
|
|
39
|
+
? ctx.ui.theme.fg("borderAccent", "mode:ask")
|
|
40
|
+
: ctx.ui.theme.fg("muted", "mode:agent");
|
|
41
|
+
ctx.ui.setStatus(MODE_STATUS_KEY, statusText);
|
|
42
|
+
}
|
|
43
|
+
function persistModeState(pi) {
|
|
44
|
+
pi.appendEntry(MODE_STATE_ENTRY_TYPE, createModeStateData(modeRuntimeStore.getSnapshot()));
|
|
45
|
+
}
|
|
46
|
+
export default function exitPlanModeExtension(pi) {
|
|
47
|
+
pi.registerTool({
|
|
48
|
+
name: "ExitPlanMode",
|
|
49
|
+
label: "ExitPlanMode",
|
|
50
|
+
description: DESCRIPTION,
|
|
51
|
+
parameters: exitPlanModeSchema,
|
|
52
|
+
async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
|
|
53
|
+
const before = modeRuntimeStore.getSnapshot();
|
|
54
|
+
if (before.activeMode !== "plan") {
|
|
55
|
+
const details = {
|
|
56
|
+
previous_mode: before.activeMode,
|
|
57
|
+
active_mode: before.activeMode,
|
|
58
|
+
changed: false,
|
|
59
|
+
plan_file: before.planFilePath,
|
|
60
|
+
};
|
|
61
|
+
return {
|
|
62
|
+
content: [{ type: "text", text: "Plan mode is not active." }],
|
|
63
|
+
details,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
const { changed, snapshot } = modeRuntimeStore.setMode("agent");
|
|
67
|
+
if (changed) {
|
|
68
|
+
persistModeState(pi);
|
|
69
|
+
}
|
|
70
|
+
updateModeStatus(ctx);
|
|
71
|
+
const details = {
|
|
72
|
+
previous_mode: before.activeMode,
|
|
73
|
+
active_mode: snapshot.activeMode,
|
|
74
|
+
changed,
|
|
75
|
+
plan_file: snapshot.planFilePath,
|
|
76
|
+
};
|
|
77
|
+
return {
|
|
78
|
+
content: [{ type: "text", text: "Exited Plan mode. Switched to Agent mode." }],
|
|
79
|
+
details,
|
|
80
|
+
};
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
}
|