codiedev 0.7.2 → 0.7.4
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/dist/commands/reverseTicket.js +68 -19
- package/dist/mcp.js +67 -21
- package/package.json +1 -1
|
@@ -36,6 +36,7 @@ function parseArgs(args) {
|
|
|
36
36
|
let base;
|
|
37
37
|
let forcedKey;
|
|
38
38
|
let noTruncate = false;
|
|
39
|
+
let dirty = false;
|
|
39
40
|
for (let i = 0; i < args.length; i++) {
|
|
40
41
|
const a = args[i];
|
|
41
42
|
if ((a === "--with" || a === "-w") && i + 1 < args.length) {
|
|
@@ -53,17 +54,20 @@ function parseArgs(args) {
|
|
|
53
54
|
else if (a === "--full") {
|
|
54
55
|
noTruncate = true;
|
|
55
56
|
}
|
|
57
|
+
else if (a === "--dirty") {
|
|
58
|
+
dirty = true;
|
|
59
|
+
}
|
|
56
60
|
else if (!a.startsWith("--") && !prUrl) {
|
|
57
61
|
prUrl = a;
|
|
58
62
|
}
|
|
59
63
|
}
|
|
60
|
-
return { prUrl, base, forcedKey, noTruncate };
|
|
64
|
+
return { prUrl, base, forcedKey, noTruncate, dirty };
|
|
61
65
|
}
|
|
62
66
|
async function runReverseTicket(args) {
|
|
63
|
-
const { prUrl, base, forcedKey, noTruncate } = parseArgs(args);
|
|
67
|
+
const { prUrl, base, forcedKey, noTruncate, dirty } = parseArgs(args);
|
|
64
68
|
// No PR URL → branch mode (mid-session, no PR exists yet).
|
|
65
69
|
if (!prUrl) {
|
|
66
|
-
return runBranchMode({ explicitBase: base, forcedKey });
|
|
70
|
+
return runBranchMode({ explicitBase: base, forcedKey, dirty });
|
|
67
71
|
}
|
|
68
72
|
const parsed = parsePrUrl(prUrl);
|
|
69
73
|
if (!parsed) {
|
|
@@ -211,10 +215,6 @@ async function runBranchMode(opts) {
|
|
|
211
215
|
}
|
|
212
216
|
const baseSha = git(`merge-base HEAD ${baseRef}`);
|
|
213
217
|
const headSha = git("rev-parse HEAD");
|
|
214
|
-
if (baseSha === headSha) {
|
|
215
|
-
console.error(`No commits on ${branch} ahead of ${baseRef} — make a commit first.`);
|
|
216
|
-
process.exit(1);
|
|
217
|
-
}
|
|
218
218
|
// Filter generated/lockfile noise from the diff body. The --stat (collected
|
|
219
219
|
// separately) still includes every file untouched, so excluded files still
|
|
220
220
|
// surface in the ticket's "Other files touched" section.
|
|
@@ -227,29 +227,78 @@ async function runBranchMode(opts) {
|
|
|
227
227
|
/(^|\/)Gemfile\.lock$/.test(p) ||
|
|
228
228
|
/(^|\/)poetry\.lock$/.test(p) ||
|
|
229
229
|
/(^|\/)go\.sum$/.test(p);
|
|
230
|
-
const
|
|
231
|
-
|
|
230
|
+
const shellTolerant = (cmd) => {
|
|
231
|
+
try {
|
|
232
|
+
return (0, child_process_1.execSync)(`${cmd} || true`, {
|
|
233
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
234
|
+
maxBuffer: 32 * 1024 * 1024,
|
|
235
|
+
})
|
|
236
|
+
.toString("utf8")
|
|
237
|
+
.trim();
|
|
238
|
+
}
|
|
239
|
+
catch {
|
|
240
|
+
return "";
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
const shQuote = (s) => `'${s.replace(/'/g, "'\\''")}'`;
|
|
244
|
+
// Default scope is committed-only — files and content reachable from HEAD,
|
|
245
|
+
// not from the working tree. Without `HEAD` as the second arg, `git diff
|
|
246
|
+
// <ref>` compares <ref> to the working tree, which silently rolls in
|
|
247
|
+
// uncommitted modifications and (via ls-files --others) untracked leftovers
|
|
248
|
+
// from prior branches. That produced audit-grade leaks where a one-commit
|
|
249
|
+
// copy-button branch generated a draft describing an unrelated auth rebrand
|
|
250
|
+
// that was sitting dirty in the working tree.
|
|
251
|
+
//
|
|
252
|
+
// --dirty re-enables the old behavior for the "ticket my current WIP"
|
|
253
|
+
// intent — explicit opt-in, since the user is asserting the dirty state is
|
|
254
|
+
// theirs and on-topic.
|
|
255
|
+
const includeDirty = opts.dirty === true;
|
|
256
|
+
const trackedRaw = git(`diff --name-only ${baseSha}${includeDirty ? "" : " HEAD"}`);
|
|
257
|
+
const trackedFiles = trackedRaw ? trackedRaw.split("\n").filter(Boolean) : [];
|
|
258
|
+
const untrackedRaw = includeDirty
|
|
259
|
+
? safeGit(() => git("ls-files --others --exclude-standard"))
|
|
260
|
+
: undefined;
|
|
261
|
+
const untrackedFiles = untrackedRaw
|
|
262
|
+
? untrackedRaw.split("\n").filter(Boolean)
|
|
263
|
+
: [];
|
|
264
|
+
const filesChanged = Array.from(new Set([...trackedFiles, ...untrackedFiles]));
|
|
232
265
|
if (filesChanged.length === 0) {
|
|
233
|
-
|
|
266
|
+
const scope = includeDirty ? "your working tree" : "HEAD";
|
|
267
|
+
console.error(`No changes detected between ${baseRef} and ${scope} on ${branch}.`);
|
|
268
|
+
if (!includeDirty) {
|
|
269
|
+
console.error("Pass --dirty to also include uncommitted + untracked changes.");
|
|
270
|
+
}
|
|
234
271
|
process.exit(1);
|
|
235
272
|
}
|
|
236
273
|
let stat = "";
|
|
237
274
|
try {
|
|
238
|
-
|
|
275
|
+
const trackedStat = git(`diff --stat=200 ${baseSha}${includeDirty ? "" : " HEAD"}`);
|
|
276
|
+
const untrackedStatLines = untrackedFiles.map((f) => {
|
|
277
|
+
const wc = shellTolerant(`wc -l < ${shQuote(f)}`);
|
|
278
|
+
const n = parseInt(wc.split(/\s+/)[0] || "0", 10) || 0;
|
|
279
|
+
return ` ${f} | ${n} ++++++++++ (new file)`;
|
|
280
|
+
});
|
|
281
|
+
stat = [trackedStat, ...untrackedStatLines]
|
|
282
|
+
.filter(Boolean)
|
|
283
|
+
.join("\n")
|
|
284
|
+
.slice(0, 8_000);
|
|
239
285
|
}
|
|
240
286
|
catch {
|
|
241
287
|
// best effort
|
|
242
288
|
}
|
|
243
289
|
let diff = "";
|
|
244
290
|
try {
|
|
245
|
-
const
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
291
|
+
const sourceTracked = trackedFiles.filter((f) => !isNoise(f));
|
|
292
|
+
const headRef = includeDirty ? "" : " HEAD";
|
|
293
|
+
const trackedDiff = sourceTracked.length > 0
|
|
294
|
+
? git(`diff ${baseSha}${headRef} -- ${sourceTracked.map(shQuote).join(" ")}`)
|
|
295
|
+
: git(`diff ${baseSha}${headRef}`);
|
|
296
|
+
const sourceUntracked = untrackedFiles.filter((f) => !isNoise(f));
|
|
297
|
+
const untrackedDiffs = sourceUntracked.map((f) => shellTolerant(`git diff --no-index --no-color -- /dev/null ${shQuote(f)}`));
|
|
298
|
+
diff = [trackedDiff, ...untrackedDiffs]
|
|
299
|
+
.filter(Boolean)
|
|
300
|
+
.join("\n")
|
|
301
|
+
.slice(0, 40_000);
|
|
253
302
|
}
|
|
254
303
|
catch {
|
|
255
304
|
// best effort
|
package/dist/mcp.js
CHANGED
|
@@ -52,7 +52,17 @@ const path = __importStar(require("path"));
|
|
|
52
52
|
const utils_1 = require("./utils");
|
|
53
53
|
const shared_1 = require("./commands/shared");
|
|
54
54
|
const PKG_NAME = "codiedev";
|
|
55
|
-
|
|
55
|
+
// Read version from the bundled package.json so the startup log + tool reports
|
|
56
|
+
// always reflect the actually-installed version, rather than a stale constant.
|
|
57
|
+
const PKG_VERSION = (() => {
|
|
58
|
+
try {
|
|
59
|
+
const pkgPath = path.resolve(__dirname, "..", "package.json");
|
|
60
|
+
return JSON.parse(fs.readFileSync(pkgPath, "utf8")).version ?? "unknown";
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return "unknown";
|
|
64
|
+
}
|
|
65
|
+
})();
|
|
56
66
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
57
67
|
// Tool definitions — descriptions tuned so Claude/Codex resolve natural-language
|
|
58
68
|
// requests into the right tool without manual steering.
|
|
@@ -1086,9 +1096,6 @@ async function handleReverseTicket(args, config) {
|
|
|
1086
1096
|
}
|
|
1087
1097
|
const baseSha = git(`merge-base HEAD ${baseRef}`);
|
|
1088
1098
|
const headSha = git("rev-parse HEAD");
|
|
1089
|
-
if (baseSha === headSha) {
|
|
1090
|
-
throw new Error(`No commits on ${branch} ahead of ${baseRef} — make a commit first.`);
|
|
1091
|
-
}
|
|
1092
1099
|
// Filter generated/lockfile noise so the LLM's diff budget gets spent on
|
|
1093
1100
|
// real source. The --stat block (collected separately below) still includes
|
|
1094
1101
|
// every file untouched, so excluded files still appear in the ticket's
|
|
@@ -1102,33 +1109,72 @@ async function handleReverseTicket(args, config) {
|
|
|
1102
1109
|
/(^|\/)Gemfile\.lock$/.test(p) ||
|
|
1103
1110
|
/(^|\/)poetry\.lock$/.test(p) ||
|
|
1104
1111
|
/(^|\/)go\.sum$/.test(p);
|
|
1105
|
-
|
|
1106
|
-
|
|
1112
|
+
// Helper: run a shell command tolerating non-zero exit codes (git diff
|
|
1113
|
+
// --no-index exits 1 when files differ, which is the normal case).
|
|
1114
|
+
// Reuses the execSync already imported above for the git() helper.
|
|
1115
|
+
const shellTolerant = (cmd) => {
|
|
1116
|
+
try {
|
|
1117
|
+
return execSync(`${cmd} || true`, {
|
|
1118
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
1119
|
+
maxBuffer: 32 * 1024 * 1024,
|
|
1120
|
+
})
|
|
1121
|
+
.toString("utf8")
|
|
1122
|
+
.trim();
|
|
1123
|
+
}
|
|
1124
|
+
catch {
|
|
1125
|
+
return "";
|
|
1126
|
+
}
|
|
1127
|
+
};
|
|
1128
|
+
const shQuote = (s) => `'${s.replace(/'/g, "'\\''")}'`;
|
|
1129
|
+
// Capture all work the developer has done relative to the base ref:
|
|
1130
|
+
// - committed changes between base and HEAD
|
|
1131
|
+
// - uncommitted modifications to tracked files
|
|
1132
|
+
// - new untracked files (path listed + content via synthetic diff)
|
|
1133
|
+
// Matches the user's intent ("ticket my current changes") rather than the
|
|
1134
|
+
// narrower "only what's been committed" interpretation.
|
|
1135
|
+
const trackedRaw = git(`diff --name-only ${baseSha}`);
|
|
1136
|
+
const trackedFiles = trackedRaw ? trackedRaw.split("\n").filter(Boolean) : [];
|
|
1137
|
+
const untrackedRaw = safeGit(() => git("ls-files --others --exclude-standard"));
|
|
1138
|
+
const untrackedFiles = untrackedRaw
|
|
1139
|
+
? untrackedRaw.split("\n").filter(Boolean)
|
|
1140
|
+
: [];
|
|
1141
|
+
const filesChanged = Array.from(new Set([...trackedFiles, ...untrackedFiles]));
|
|
1107
1142
|
if (filesChanged.length === 0) {
|
|
1108
|
-
throw new Error(`No
|
|
1143
|
+
throw new Error(`No changes detected between ${baseRef} and your working tree on ${branch}.`);
|
|
1109
1144
|
}
|
|
1110
|
-
//
|
|
1111
|
-
//
|
|
1112
|
-
//
|
|
1145
|
+
// --stat block: compact ground truth for "every file touched". Tracked
|
|
1146
|
+
// changes from `git diff --stat baseSha`; untracked files appended as
|
|
1147
|
+
// synthesized stat rows so the LLM sees them with line counts.
|
|
1113
1148
|
let stat = "";
|
|
1114
1149
|
try {
|
|
1115
|
-
|
|
1150
|
+
const trackedStat = git(`diff --stat=200 ${baseSha}`);
|
|
1151
|
+
const untrackedStatLines = untrackedFiles.map((f) => {
|
|
1152
|
+
const wc = shellTolerant(`wc -l < ${shQuote(f)}`);
|
|
1153
|
+
const n = parseInt(wc.split(/\s+/)[0] || "0", 10) || 0;
|
|
1154
|
+
return ` ${f} | ${n} ++++++++++ (new file)`;
|
|
1155
|
+
});
|
|
1156
|
+
stat = [trackedStat, ...untrackedStatLines]
|
|
1157
|
+
.filter(Boolean)
|
|
1158
|
+
.join("\n")
|
|
1159
|
+
.slice(0, 8_000);
|
|
1116
1160
|
}
|
|
1117
1161
|
catch {
|
|
1118
1162
|
// Best effort.
|
|
1119
1163
|
}
|
|
1120
|
-
// Diff body: 40k cap, noise-filtered via
|
|
1121
|
-
//
|
|
1164
|
+
// Diff body: 40k cap, noise-filtered. Tracked changes via `git diff baseSha`,
|
|
1165
|
+
// plus synthetic add-only diffs for untracked files via `git diff --no-index`.
|
|
1122
1166
|
let diff = "";
|
|
1123
1167
|
try {
|
|
1124
|
-
const
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1168
|
+
const sourceTracked = trackedFiles.filter((f) => !isNoise(f));
|
|
1169
|
+
const trackedDiff = sourceTracked.length > 0
|
|
1170
|
+
? git(`diff ${baseSha} -- ${sourceTracked.map(shQuote).join(" ")}`)
|
|
1171
|
+
: git(`diff ${baseSha}`);
|
|
1172
|
+
const sourceUntracked = untrackedFiles.filter((f) => !isNoise(f));
|
|
1173
|
+
const untrackedDiffs = sourceUntracked.map((f) => shellTolerant(`git diff --no-index --no-color -- /dev/null ${shQuote(f)}`));
|
|
1174
|
+
diff = [trackedDiff, ...untrackedDiffs]
|
|
1175
|
+
.filter(Boolean)
|
|
1176
|
+
.join("\n")
|
|
1177
|
+
.slice(0, 40_000);
|
|
1132
1178
|
}
|
|
1133
1179
|
catch {
|
|
1134
1180
|
// Best effort.
|
package/package.json
CHANGED