@astrosheep/keiyaku 0.1.77 → 0.1.78

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.
Files changed (49) hide show
  1. package/build/.tsbuildinfo +1 -1
  2. package/build/config/apply-argument-descriptions.js +1 -1
  3. package/build/config/base-rules.js +2 -1
  4. package/build/config/dotenv.js +2 -1
  5. package/build/config/settings.js +2 -7
  6. package/build/config/term-presets/resolver.js +0 -3
  7. package/build/errno.js +3 -0
  8. package/build/flow-error.js +2 -0
  9. package/build/generated/version.js +1 -1
  10. package/build/git/diff/constants.js +1 -0
  11. package/build/git/diff/filter.js +3 -18
  12. package/build/git/diff/parsers.js +149 -61
  13. package/build/git/diff/preview.js +16 -2
  14. package/build/git/diff/read.js +32 -20
  15. package/build/git/snapshot.js +5 -24
  16. package/build/git/worktree.js +5 -4
  17. package/build/mcp/server.js +61 -69
  18. package/build/protocol/draft-artifacts.js +2 -1
  19. package/build/protocol/file-guards.js +2 -1
  20. package/build/protocol/markdown/lex.js +52 -14
  21. package/build/protocol/markdown/normalization.js +3 -2
  22. package/build/protocol/markdown/parser.js +2 -2
  23. package/build/protocol/response-history.js +44 -5
  24. package/build/protocol/status-previews.js +20 -8
  25. package/build/protocol/summon-draft.js +3 -2
  26. package/build/protocol/summon-input.js +1 -0
  27. package/build/tools/amend/index.js +11 -21
  28. package/build/tools/ask/index.js +11 -18
  29. package/build/tools/ask/persist.js +60 -37
  30. package/build/tools/ask/run.js +15 -5
  31. package/build/tools/create-handler.js +31 -0
  32. package/build/tools/drive/index.js +11 -24
  33. package/build/tools/drive/run.js +9 -5
  34. package/build/tools/petition/claim-gates.js +23 -1
  35. package/build/tools/petition/claim.js +19 -2
  36. package/build/tools/petition/forfeit.js +4 -1
  37. package/build/tools/petition/index.js +43 -58
  38. package/build/tools/petition/run.js +12 -0
  39. package/build/tools/round/head-guard.js +10 -0
  40. package/build/tools/round/incremental-diff.js +6 -2
  41. package/build/tools/round/report.js +24 -2
  42. package/build/tools/round/run.js +6 -0
  43. package/build/tools/round/worktree.js +6 -2
  44. package/build/tools/status/index.js +11 -24
  45. package/build/tools/status/read.js +6 -4
  46. package/build/tools/summon/index.js +17 -27
  47. package/build/tools/summon/run.js +21 -18
  48. package/package.json +6 -6
  49. package/build/git/diff/stat.js +0 -9
@@ -4,3 +4,4 @@ export const MAX_DIFF_LINES_PER_FILE = 80;
4
4
  export const INCREMENTAL_DIFF_RANGE = "HEAD~1...HEAD";
5
5
  export const TARGETED_DIFF_WARNING = "Warning: no valid diff coordinates found in KEIYAKU_TRACE.md; showing stat-only diff.";
6
6
  export const NOT_SHOWN_IN_DETAIL_PREFIX = "--- not shown in detail: ";
7
+ export const DIFF_TRUNCATED_MARKER = "...[truncated]";
@@ -1,5 +1,5 @@
1
1
  import { NOT_SHOWN_IN_DETAIL_PREFIX } from "./constants.js";
2
- import { isDeletedDiffSection, parseDiffPathFromHeader, parseUnifiedHunks, splitDiffByFile, truncateTextWithFooter, } from "./parsers.js";
2
+ import { isDeletedDiffSection, parseDiffPathFromHeader, parseUnifiedHunks, splitDiffByFile, truncateDiffSectionByLineCount, } from "./parsers.js";
3
3
  function rangesOverlap(aStart, aEnd, bStart, bEnd) {
4
4
  return aStart <= bEnd && bStart <= aEnd;
5
5
  }
@@ -12,21 +12,6 @@ function buildCoordinateIndex(coordinates) {
12
12
  }
13
13
  return index;
14
14
  }
15
- export function capTargetedDiffText(diffText, maxChars) {
16
- if (diffText.length <= maxChars)
17
- return diffText;
18
- return truncateTextWithFooter(diffText, maxChars, diffText.length - maxChars);
19
- }
20
- function capDiffSectionByLineCount(section, maxLinesPerFile) {
21
- if (maxLinesPerFile < 1)
22
- return "";
23
- const lines = section.split("\n");
24
- if (lines.length <= maxLinesPerFile)
25
- return section;
26
- const truncated = lines.slice(0, maxLinesPerFile);
27
- truncated.push(`... [truncated ${lines.length - maxLinesPerFile} lines for this file]`);
28
- return truncated.join("\n");
29
- }
30
15
  export function filterDiffByCoordinates(rawPatch, coordinates, maxLinesPerFile = Number.POSITIVE_INFINITY) {
31
16
  const sections = splitDiffByFile(rawPatch);
32
17
  if (sections.length === 0)
@@ -66,11 +51,11 @@ export function filterDiffByCoordinates(rawPatch, coordinates, maxLinesPerFile =
66
51
  ...prelude,
67
52
  ...matchingHunks.flatMap((hunk) => [hunk.header, ...hunk.lines]),
68
53
  ].join("\n");
69
- selectedSectionsByPath.set(coordinate.path, capDiffSectionByLineCount(rendered, maxLinesPerFile));
54
+ selectedSectionsByPath.set(coordinate.path, truncateDiffSectionByLineCount(rendered, maxLinesPerFile));
70
55
  }
71
56
  const selectedSections = [
72
57
  ...selectedSectionsByPath.values(),
73
- ...deletedSections.map((section) => capDiffSectionByLineCount(section, maxLinesPerFile)),
58
+ ...deletedSections.map((section) => truncateDiffSectionByLineCount(section, maxLinesPerFile)),
74
59
  ];
75
60
  if (selectedSections.length === 0)
76
61
  return "";
@@ -1,58 +1,17 @@
1
- const SIMPLE_NAME_STATUS_VALUES = ["A", "M", "D", "T", "U", "X", "B"];
2
- const SIMPLE_NAME_STATUS_SET = new Set(SIMPLE_NAME_STATUS_VALUES);
3
- function isSimpleNameStatus(status) {
4
- return SIMPLE_NAME_STATUS_SET.has(status);
5
- }
6
- export function parseNumStat(content) {
7
- const rows = [];
8
- for (const line of content.split(/\r?\n/)) {
9
- if (!line.trim())
10
- continue;
11
- const [addRaw, delRaw, ...pathParts] = line.split("\t");
12
- const filePath = pathParts.join("\t").trim();
13
- if (!filePath)
14
- continue;
15
- const binary = addRaw === "-" || delRaw === "-";
16
- rows.push({
17
- path: filePath,
18
- additions: binary ? 0 : Number.parseInt(addRaw, 10) || 0,
19
- deletions: binary ? 0 : Number.parseInt(delRaw, 10) || 0,
20
- binary,
21
- });
22
- }
23
- return rows;
24
- }
25
- export function parseNameStatus(content) {
26
- const map = new Map();
27
- for (const line of content.split(/\r?\n/)) {
28
- if (!line.trim())
29
- continue;
30
- const parts = line.split("\t");
31
- const statusRaw = (parts[0] ?? "").trim();
32
- if (!statusRaw)
33
- continue;
34
- if ((statusRaw.startsWith("R") || statusRaw.startsWith("C")) && parts.length >= 3) {
35
- const scoreRaw = statusRaw.slice(1);
36
- const score = Number.parseInt(scoreRaw, 10);
37
- const oldPath = (parts[1] ?? "").trim();
38
- const path = (parts[2] ?? "").trim();
39
- if (!path)
40
- continue;
41
- const status = statusRaw[0] === "R" ? "R" : "C";
42
- map.set(path, { status, score: Number.isFinite(score) ? score : 0, oldPath, path });
43
- continue;
44
- }
45
- const path = (parts[1] ?? "").trim();
46
- if (!path)
47
- continue;
48
- // Git can emit single-letter statuses, possibly in combinations; we keep just the first.
49
- const status = statusRaw[0] ?? "";
50
- if (!isSimpleNameStatus(status))
51
- continue;
52
- map.set(path, { status, path });
53
- }
54
- return map;
55
- }
1
+ import { Buffer } from "node:buffer";
2
+ import { DIFF_TRUNCATED_MARKER } from "./constants.js";
3
+ const DIFF_GIT_HEADER_PREFIX = "diff --git ";
4
+ const GIT_QUOTED_PATH_ESCAPES = {
5
+ '"': 34,
6
+ "\\": 92,
7
+ a: 7,
8
+ b: 8,
9
+ f: 12,
10
+ n: 10,
11
+ r: 13,
12
+ t: 9,
13
+ v: 11,
14
+ };
56
15
  export function splitDiffByFile(content) {
57
16
  const sections = [];
58
17
  let current = [];
@@ -135,10 +94,83 @@ export function isMissingBaseRevisionError(err) {
135
94
  text.includes("unknown revision or path not in the working tree") ||
136
95
  text.includes("ambiguous argument"));
137
96
  }
97
+ function unescapeQuotedDiffPath(value) {
98
+ const bytes = [];
99
+ for (let index = 0; index < value.length; index += 1) {
100
+ const char = value[index];
101
+ if (char !== "\\") {
102
+ bytes.push(...Buffer.from(char ?? "", "utf8"));
103
+ continue;
104
+ }
105
+ const escaped = value[index + 1];
106
+ if (escaped === undefined) {
107
+ bytes.push(92);
108
+ continue;
109
+ }
110
+ if (escaped >= "0" && escaped <= "7") {
111
+ let octal = escaped;
112
+ let cursor = index + 2;
113
+ while (cursor < value.length && octal.length < 3) {
114
+ const digit = value[cursor];
115
+ if (digit === undefined || digit < "0" || digit > "7")
116
+ break;
117
+ octal += digit;
118
+ cursor += 1;
119
+ }
120
+ bytes.push(Number.parseInt(octal, 8));
121
+ index = cursor - 1;
122
+ continue;
123
+ }
124
+ const decoded = GIT_QUOTED_PATH_ESCAPES[escaped];
125
+ if (decoded !== undefined) {
126
+ bytes.push(decoded);
127
+ index += 1;
128
+ continue;
129
+ }
130
+ bytes.push(...Buffer.from(escaped, "utf8"));
131
+ index += 1;
132
+ }
133
+ return Buffer.from(bytes).toString("utf8");
134
+ }
135
+ function readDiffHeaderToken(line, startIndex) {
136
+ let index = startIndex;
137
+ while (index < line.length && line[index] === " ") {
138
+ index += 1;
139
+ }
140
+ if (index >= line.length)
141
+ return null;
142
+ if (line[index] !== '"') {
143
+ let endIndex = index;
144
+ while (endIndex < line.length && line[endIndex] !== " ") {
145
+ endIndex += 1;
146
+ }
147
+ return {
148
+ value: line.slice(index, endIndex),
149
+ nextIndex: endIndex,
150
+ };
151
+ }
152
+ let endIndex = index + 1;
153
+ let escaped = false;
154
+ while (endIndex < line.length) {
155
+ const char = line[endIndex];
156
+ if (!escaped && char === '"') {
157
+ return {
158
+ value: unescapeQuotedDiffPath(line.slice(index + 1, endIndex)),
159
+ nextIndex: endIndex + 1,
160
+ };
161
+ }
162
+ escaped = !escaped && char === "\\";
163
+ endIndex += 1;
164
+ }
165
+ return null;
166
+ }
138
167
  export function parseDiffPathFromHeader(diffGitLine) {
139
- // Format: diff --git a/<path> b/<path>
140
- const parts = diffGitLine.split(" ");
141
- const bPart = parts[3] ?? "";
168
+ if (!diffGitLine.startsWith(DIFF_GIT_HEADER_PREFIX))
169
+ return null;
170
+ const aPart = readDiffHeaderToken(diffGitLine, DIFF_GIT_HEADER_PREFIX.length);
171
+ if (!aPart)
172
+ return null;
173
+ const bPart = readDiffHeaderToken(diffGitLine, aPart.nextIndex)?.value ?? "";
142
174
  if (!bPart.startsWith("b/"))
143
175
  return null;
144
176
  return bPart.slice(2);
@@ -158,20 +190,76 @@ export function truncateTextWithFooter(text, maxChars, omittedChars) {
158
190
  const cut = text.slice(0, maxChars - footer.length);
159
191
  return `${cut}${footer}`;
160
192
  }
161
- export function truncateBlockToLineBudget(block, maxChars) {
193
+ function isChangedDiffLine(line) {
194
+ return (line.startsWith("+") && !line.startsWith("+++")) || (line.startsWith("-") && !line.startsWith("---"));
195
+ }
196
+ function trimUnsafeDiffBoundary(kept, remaining) {
197
+ if (kept.length === 0 || remaining.length === 0)
198
+ return kept;
199
+ const safe = [...kept];
200
+ while (safe.length > 0 && isChangedDiffLine(safe[safe.length - 1] ?? "") && isChangedDiffLine(remaining[0] ?? "")) {
201
+ safe.pop();
202
+ }
203
+ if ((safe[safe.length - 1] ?? "").startsWith("@@ ")) {
204
+ safe.pop();
205
+ }
206
+ return safe;
207
+ }
208
+ function renderTruncatedDiff(lines) {
209
+ if (lines.length === 0)
210
+ return DIFF_TRUNCATED_MARKER;
211
+ return `${lines.join("\n")}\n${DIFF_TRUNCATED_MARKER}`;
212
+ }
213
+ export function truncateDiffSectionByLineCount(section, maxLines) {
214
+ if (maxLines < 1)
215
+ return "";
216
+ const lines = section.split("\n");
217
+ if (lines.length <= maxLines)
218
+ return section;
219
+ const { prelude, hunks } = parseUnifiedHunks(lines);
220
+ if (hunks.length === 0 || prelude.length >= maxLines) {
221
+ return renderTruncatedDiff(trimUnsafeDiffBoundary(lines.slice(0, maxLines), lines.slice(maxLines)));
222
+ }
223
+ const kept = [...prelude];
224
+ let lineCount = prelude.length;
225
+ for (let index = 0; index < hunks.length; index += 1) {
226
+ const hunk = hunks[index];
227
+ const hunkLines = [hunk.header, ...hunk.lines];
228
+ if (lineCount + hunkLines.length <= maxLines) {
229
+ kept.push(...hunkLines);
230
+ lineCount += hunkLines.length;
231
+ continue;
232
+ }
233
+ kept.push(...hunkLines);
234
+ lineCount += hunkLines.length;
235
+ break;
236
+ }
237
+ if (lineCount >= lines.length)
238
+ return section;
239
+ return renderTruncatedDiff(kept);
240
+ }
241
+ export function truncateDiffBlockToCharBudget(block, maxChars) {
162
242
  if (maxChars <= 0)
163
243
  return "";
164
244
  if (block.length <= maxChars)
165
245
  return block;
246
+ if (DIFF_TRUNCATED_MARKER.length >= maxChars) {
247
+ return DIFF_TRUNCATED_MARKER.slice(0, maxChars);
248
+ }
166
249
  const lines = block.split("\n");
167
250
  const kept = [];
168
251
  let used = 0;
169
252
  for (const line of lines) {
170
253
  const delta = (kept.length > 0 ? 1 : 0) + line.length;
171
- if (used + delta > maxChars)
254
+ const reservedMarker = 1 + DIFF_TRUNCATED_MARKER.length;
255
+ if (used + delta + reservedMarker > maxChars)
172
256
  break;
173
257
  kept.push(line);
174
258
  used += delta;
175
259
  }
176
- return kept.join("\n");
260
+ const safe = trimUnsafeDiffBoundary(kept, lines.slice(kept.length));
261
+ if (safe.length === 0) {
262
+ return DIFF_TRUNCATED_MARKER.slice(0, maxChars);
263
+ }
264
+ return renderTruncatedDiff(safe);
177
265
  }
@@ -1,6 +1,5 @@
1
1
  import { createGit, wrapGitError } from "../core.js";
2
2
  import { DIFF_EXCLUDES } from "./constants.js";
3
- import { parseNumStat } from "./parsers.js";
4
3
  export async function getDiffStats(cwd, baseBranch) {
5
4
  const git = createGit(cwd);
6
5
  let rawNumStat;
@@ -10,7 +9,22 @@ export async function getDiffStats(cwd, baseBranch) {
10
9
  catch (err) {
11
10
  throw wrapGitError(`diff --numstat ${baseBranch}...HEAD`, err, cwd);
12
11
  }
13
- const rows = parseNumStat(rawNumStat);
12
+ const rows = rawNumStat
13
+ .split(/\r?\n/)
14
+ .flatMap((line) => {
15
+ if (!line.trim())
16
+ return [];
17
+ const [addRaw, delRaw, ...pathParts] = line.split("\t");
18
+ const filePath = pathParts.join("\t").trim();
19
+ if (!filePath)
20
+ return [];
21
+ const binary = addRaw === "-" || delRaw === "-";
22
+ return [{
23
+ path: filePath,
24
+ additions: binary ? 0 : Number.parseInt(addRaw, 10) || 0,
25
+ deletions: binary ? 0 : Number.parseInt(delRaw, 10) || 0,
26
+ }];
27
+ });
14
28
  return {
15
29
  filesChanged: rows.length,
16
30
  insertions: rows.reduce((sum, row) => sum + row.additions, 0),
@@ -1,13 +1,18 @@
1
1
  import { createGit, wrapGitError } from "../core.js";
2
2
  import { DIFF_EXCLUDES } from "./constants.js";
3
- import { isMissingBaseRevisionError, splitDiffByFile, totalJoinedLength, truncateBlockToLineBudget, } from "./parsers.js";
4
- import { renderStatDiffPreviewText } from "./stat.js";
5
- function buildPatchCommand(range, options) {
3
+ import { isMissingBaseRevisionError, splitDiffByFile, totalJoinedLength, truncateDiffBlockToCharBudget, truncateDiffSectionByLineCount, truncateTextWithFooter, } from "./parsers.js";
4
+ function normalizeDiffTarget(target) {
5
+ return typeof target === "string" ? [target] : [...target];
6
+ }
7
+ function formatDiffTarget(target) {
8
+ return typeof target === "string" ? target : target.join(" ");
9
+ }
10
+ function buildPatchCommand(target, options) {
6
11
  const args = ["diff", "--no-color", "--no-ext-diff", "--unified=3"];
7
12
  if (options.algorithm === "histogram") {
8
13
  args.push("--diff-algorithm=histogram");
9
14
  }
10
- args.push(range, "--", ".", ...DIFF_EXCLUDES);
15
+ args.push(...normalizeDiffTarget(target), "--", ".", ...DIFF_EXCLUDES);
11
16
  return args;
12
17
  }
13
18
  function renderPatchDiff(output, options) {
@@ -26,11 +31,9 @@ function renderPatchDiff(output, options) {
26
31
  let omittedFiles = 0;
27
32
  for (let i = 0; i < sections.length; i += 1) {
28
33
  const section = sections[i];
29
- const content = maxLinesPerFile === undefined ? [...section] : section.slice(0, maxLinesPerFile);
30
- if (maxLinesPerFile !== undefined && section.length > maxLinesPerFile) {
31
- content.push(`... [truncated ${section.length - maxLinesPerFile} lines for this file]`);
32
- }
33
- const fileBlock = content.join("\n");
34
+ const fileBlock = maxLinesPerFile === undefined
35
+ ? section.join("\n")
36
+ : truncateDiffSectionByLineCount(section.join("\n"), maxLinesPerFile);
34
37
  if (maxChars === undefined) {
35
38
  renderedSections.push(fileBlock);
36
39
  continue;
@@ -44,7 +47,7 @@ function renderPatchDiff(output, options) {
44
47
  if (renderedSections.length === 0) {
45
48
  const notice = `... [omitted ${omittedFiles} file(s) to stay under ${maxChars} chars] ...`;
46
49
  const partialBudget = Math.max(0, maxChars - totalJoinedLength(["", notice]));
47
- const partial = truncateBlockToLineBudget(fileBlock, partialBudget);
50
+ const partial = truncateDiffBlockToCharBudget(fileBlock, partialBudget);
48
51
  if (partial.trim()) {
49
52
  renderedSections.push(partial);
50
53
  }
@@ -62,20 +65,29 @@ function renderPatchDiff(output, options) {
62
65
  }
63
66
  return renderedSections.join("\n\n").trim() || "No diff.";
64
67
  }
65
- export async function readDiff(cwd, range, format, options = {}) {
68
+ export async function readDiff(cwd, target, format, options = {}) {
66
69
  const git = createGit(cwd);
70
+ const formattedTarget = formatDiffTarget(target);
67
71
  try {
68
72
  if (format === "stat") {
69
73
  const statVariant = options.statVariant ?? "summary";
70
74
  const args = statVariant === "numstat"
71
- ? ["diff", "--numstat", range, "--", ".", ...DIFF_EXCLUDES]
72
- : ["diff", "--stat=120,80", "--compact-summary", range, "--", ".", ...DIFF_EXCLUDES];
75
+ ? ["diff", "--numstat", ...normalizeDiffTarget(target), "--", ".", ...DIFF_EXCLUDES]
76
+ : ["diff", "--stat=120,80", "--compact-summary", ...normalizeDiffTarget(target), "--", ".", ...DIFF_EXCLUDES];
73
77
  const output = await git.raw(args);
74
- return statVariant === "summary"
75
- ? renderStatDiffPreviewText(output, options.maxChars ?? Number.POSITIVE_INFINITY)
76
- : output.trim() || "No diff.";
78
+ if (statVariant !== "summary") {
79
+ return output.trim() || "No diff.";
80
+ }
81
+ const trimmed = output.trim();
82
+ if (!trimmed)
83
+ return "No diff.";
84
+ const maxChars = options.maxChars ?? Number.POSITIVE_INFINITY;
85
+ if (trimmed.length <= maxChars) {
86
+ return trimmed;
87
+ }
88
+ return truncateTextWithFooter(trimmed, maxChars, trimmed.length - maxChars);
77
89
  }
78
- const output = await git.raw(buildPatchCommand(range, options));
90
+ const output = await git.raw(buildPatchCommand(target, options));
79
91
  return renderPatchDiff(output, options);
80
92
  }
81
93
  catch (error) {
@@ -84,9 +96,9 @@ export async function readDiff(cwd, range, format, options = {}) {
84
96
  }
85
97
  const commandLabel = format === "stat"
86
98
  ? options.statVariant === "numstat"
87
- ? `diff --numstat ${range}`
88
- : `diff --stat=120,80 --compact-summary ${range}`
89
- : `diff --no-color --no-ext-diff --unified=3 ${range}`;
99
+ ? `diff --numstat ${formattedTarget}`
100
+ : `diff --stat=120,80 --compact-summary ${formattedTarget}`
101
+ : `diff --no-color --no-ext-diff --unified=3 ${formattedTarget}`;
90
102
  throw wrapGitError(commandLabel, error, cwd);
91
103
  }
92
104
  }
@@ -1,7 +1,7 @@
1
1
  import * as fs from "fs/promises";
2
2
  import * as os from "os";
3
3
  import * as path from "path";
4
- import { MISSING_HEAD_PATTERNS, NOT_GIT_REPOSITORY_PATTERNS, createGit, errorContainsAnyPattern, wrapGitError } from "./core.js";
4
+ import { NOT_GIT_REPOSITORY_PATTERNS, createGit, errorContainsAnyPattern, wrapGitError } from "./core.js";
5
5
  async function withTempIndex(cwd, run) {
6
6
  const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "keiyaku-git-index-"));
7
7
  const tempIndexPath = path.join(tempDir, "index");
@@ -18,26 +18,8 @@ export async function createWorkspaceSnapshot(cwd) {
18
18
  try {
19
19
  return await withTempIndex(cwd, async (git) => {
20
20
  await git.raw(["add", "-A"]);
21
- const tree = (await git.raw(["write-tree"])).trim();
22
- if (!tree)
23
- return undefined;
24
- let head;
25
- try {
26
- head = (await git.revparse(["--verify", "HEAD"])).trim() || undefined;
27
- }
28
- catch (error) {
29
- if (!errorContainsAnyPattern(error, MISSING_HEAD_PATTERNS)) {
30
- throw wrapGitError("rev-parse --verify HEAD", error, cwd);
31
- }
32
- head = undefined;
33
- }
34
- const args = ["commit-tree", tree];
35
- if (head) {
36
- args.push("-p", head);
37
- }
38
- args.push("-m", "keiyaku ask snapshot");
39
- const sha = await git.raw(args);
40
- return sha.trim() || undefined;
21
+ const tree = await git.raw(["write-tree"]);
22
+ return tree.trim() || undefined;
41
23
  });
42
24
  }
43
25
  catch (error) {
@@ -50,11 +32,10 @@ export async function createWorkspaceSnapshot(cwd) {
50
32
  export async function getChangedFilesBetweenSnapshots(cwd, beforeSha, afterSha) {
51
33
  const git = createGit(cwd);
52
34
  try {
53
- const output = await git.raw(["diff", "--name-status", `${beforeSha}..${afterSha}`]);
35
+ const output = await git.raw(["diff", "--name-status", beforeSha, afterSha]);
54
36
  return output.trim();
55
37
  }
56
38
  catch (err) {
57
- throw wrapGitError(`diff --name-status ${beforeSha}..${afterSha}`, err, cwd);
39
+ throw wrapGitError(`diff --name-status ${beforeSha} ${afterSha}`, err, cwd);
58
40
  }
59
41
  }
60
- export { withTempIndex };
@@ -6,6 +6,7 @@ export const DIRTY_FILE_CATEGORY = {
6
6
  renamed: "renamed",
7
7
  conflicted: "conflicted",
8
8
  copied: "copied",
9
+ untracked: "untracked",
9
10
  };
10
11
  function normalizeGitPath(filePath) {
11
12
  return filePath.trim().replace(/^\.\/+/, "");
@@ -22,6 +23,9 @@ function toDirtyStatusPathSets(status) {
22
23
  };
23
24
  }
24
25
  function deriveDirtyFileCategory(file, statusPathSets) {
26
+ if (statusPathSets.untracked.has(file.path)) {
27
+ return DIRTY_FILE_CATEGORY.untracked;
28
+ }
25
29
  if (statusPathSets.conflicted.has(file.path)) {
26
30
  return DIRTY_FILE_CATEGORY.conflicted;
27
31
  }
@@ -56,10 +60,7 @@ export async function getDirtyFiles(cwd) {
56
60
  throw wrapGitError("status --porcelain", err, cwd);
57
61
  }
58
62
  const statusPathSets = toDirtyStatusPathSets(status);
59
- return status.files
60
- .filter((file) => !(file.index === "?" && file.working_dir === "?"))
61
- .filter((file) => !statusPathSets.untracked.has(file.path))
62
- .map((file) => ({
63
+ return status.files.map((file) => ({
63
64
  path: file.path,
64
65
  index: file.index,
65
66
  working_dir: file.working_dir,
@@ -1,7 +1,7 @@
1
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import { resolveOath } from "../protocol/oath.js";
3
- import { amendToolSchema, askToolSchemaBase, attachAskResponsePathValidation, summonToolSchema, driveToolSchema, petitionToolSchema, helpToolSchema, statusToolSchema, } from "../tools/schema.js";
4
- import { getAvailableNamesForPreset, listTermPresets, resolveTermPreset } from "../config/term-presets/resolver.js";
3
+ import { amendToolSchema, askToolSchema, summonToolSchema, driveToolSchema, petitionToolSchema, helpToolSchema, statusToolSchema, } from "../tools/schema.js";
4
+ import { getAvailableNamesForPreset, resolveTermPreset } from "../config/term-presets/resolver.js";
5
5
  import { renderPreset } from "../config/render-template.js";
6
6
  import { applyArgumentDescriptions } from "../config/apply-argument-descriptions.js";
7
7
  import { createAmendHandler } from "../tools/amend/index.js";
@@ -13,9 +13,6 @@ import { createHelpHandler } from "../tools/help.js";
13
13
  import { createStatusHandler } from "../tools/status/index.js";
14
14
  function registerTools(server, packageVersion) {
15
15
  const preset = resolveTermPreset();
16
- const presetIdentities = listTermPresets()
17
- .map((item) => `${item.id}=${item.identity}`)
18
- .join(", ");
19
16
  const availableNames = getAvailableNamesForPreset(preset).join(", ");
20
17
  const currentOath = resolveOath();
21
18
  const renderedPreset = renderPreset(preset, {
@@ -25,73 +22,68 @@ function registerTools(server, packageVersion) {
25
22
  amend: preset.tools.amend.name,
26
23
  close: preset.tools.close.name,
27
24
  identity: preset.identity,
28
- preset_identities: presetIdentities,
29
25
  available_names: availableNames,
30
26
  oath_text: `'${currentOath}'`,
31
27
  });
32
- const startPreset = renderedPreset.tools.start;
33
- const drivePreset = renderedPreset.tools.drive;
34
- const askPreset = renderedPreset.tools.ask;
35
- const amendPreset = renderedPreset.tools.amend;
36
- const closePreset = renderedPreset.tools.close;
37
- const helpPreset = renderedPreset.tools.help;
38
- const statusPreset = renderedPreset.tools.status;
39
- const dynamicCloseSchema = applyArgumentDescriptions(petitionToolSchema, {
40
- ...closePreset.args,
41
- });
42
- const dynamicStartSchema = applyArgumentDescriptions(summonToolSchema, {
43
- ...startPreset.args,
44
- });
45
- const dynamicDriveSchema = applyArgumentDescriptions(driveToolSchema, {
46
- ...drivePreset.args,
47
- });
48
- const dynamicAskSchema = attachAskResponsePathValidation(applyArgumentDescriptions(askToolSchemaBase, {
49
- ...askPreset.args,
50
- }));
51
- const dynamicAmendSchema = applyArgumentDescriptions(amendToolSchema, {
52
- ...amendPreset.args,
53
- });
54
- const dynamicHelpSchema = applyArgumentDescriptions(helpToolSchema, {
55
- ...helpPreset.args,
56
- });
57
- const dynamicStatusSchema = applyArgumentDescriptions(statusToolSchema, {
58
- ...statusPreset.args,
59
- });
60
- server.registerTool(startPreset.name, {
61
- title: startPreset.title,
62
- description: startPreset.description,
63
- inputSchema: dynamicStartSchema,
64
- }, createSummonHandler());
65
- server.registerTool(drivePreset.name, {
66
- title: drivePreset.title,
67
- description: drivePreset.description,
68
- inputSchema: dynamicDriveSchema,
69
- }, createDriveHandler());
70
- server.registerTool(askPreset.name, {
71
- title: askPreset.title,
72
- description: askPreset.description,
73
- inputSchema: dynamicAskSchema,
74
- }, createAskHandler());
75
- server.registerTool(amendPreset.name, {
76
- title: amendPreset.title,
77
- description: amendPreset.description,
78
- inputSchema: dynamicAmendSchema,
79
- }, createAmendHandler());
80
- server.registerTool(closePreset.name, {
81
- title: closePreset.title,
82
- description: closePreset.description,
83
- inputSchema: dynamicCloseSchema,
84
- }, createPetitionHandler());
85
- server.registerTool(helpPreset.name, {
86
- title: helpPreset.title,
87
- description: helpPreset.description,
88
- inputSchema: dynamicHelpSchema,
89
- }, createHelpHandler(renderedPreset, packageVersion));
90
- server.registerTool(statusPreset.name, {
91
- title: statusPreset.title,
92
- description: statusPreset.description,
93
- inputSchema: dynamicStatusSchema,
94
- }, createStatusHandler());
28
+ const tools = [
29
+ {
30
+ key: "start",
31
+ schema: applyArgumentDescriptions(summonToolSchema, {
32
+ ...renderedPreset.tools.start.args,
33
+ }),
34
+ handler: createSummonHandler(),
35
+ },
36
+ {
37
+ key: "drive",
38
+ schema: applyArgumentDescriptions(driveToolSchema, {
39
+ ...renderedPreset.tools.drive.args,
40
+ }),
41
+ handler: createDriveHandler(),
42
+ },
43
+ {
44
+ key: "ask",
45
+ schema: applyArgumentDescriptions(askToolSchema, {
46
+ ...renderedPreset.tools.ask.args,
47
+ }),
48
+ handler: createAskHandler(),
49
+ },
50
+ {
51
+ key: "amend",
52
+ schema: applyArgumentDescriptions(amendToolSchema, {
53
+ ...renderedPreset.tools.amend.args,
54
+ }),
55
+ handler: createAmendHandler(),
56
+ },
57
+ {
58
+ key: "close",
59
+ schema: applyArgumentDescriptions(petitionToolSchema, {
60
+ ...renderedPreset.tools.close.args,
61
+ }),
62
+ handler: createPetitionHandler(),
63
+ },
64
+ {
65
+ key: "help",
66
+ schema: applyArgumentDescriptions(helpToolSchema, {
67
+ ...renderedPreset.tools.help.args,
68
+ }),
69
+ handler: createHelpHandler(renderedPreset, packageVersion),
70
+ },
71
+ {
72
+ key: "status",
73
+ schema: applyArgumentDescriptions(statusToolSchema, {
74
+ ...renderedPreset.tools.status.args,
75
+ }),
76
+ handler: createStatusHandler(),
77
+ },
78
+ ];
79
+ for (const tool of tools) {
80
+ const toolPreset = renderedPreset.tools[tool.key];
81
+ server.registerTool(toolPreset.name, {
82
+ title: toolPreset.title,
83
+ description: toolPreset.description,
84
+ inputSchema: tool.schema,
85
+ }, tool.handler);
86
+ }
95
87
  }
96
88
  export function createServer(packageVersion) {
97
89
  const server = new McpServer({