@shanepadgett/tau-agent 0.27.0 → 0.28.1

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 (113) hide show
  1. package/extensions/context/sync.ts +1 -9
  2. package/extensions/context-pruning/prune.ts +2 -2
  3. package/extensions/explore/README.md +13 -21
  4. package/extensions/explore/ast/adapter.ts +97 -0
  5. package/extensions/explore/ast/engine.ts +470 -0
  6. package/extensions/explore/ast/format/ast-search.ts +97 -0
  7. package/extensions/explore/ast/format/composite.ts +26 -0
  8. package/extensions/explore/ast/format/context.ts +66 -0
  9. package/extensions/explore/ast/format/deps.ts +86 -0
  10. package/extensions/explore/ast/format/discover.ts +85 -0
  11. package/extensions/explore/ast/format/impact.ts +105 -0
  12. package/extensions/explore/ast/format/outline.ts +66 -0
  13. package/extensions/explore/ast/format/relationships.ts +81 -0
  14. package/extensions/explore/ast/format/show.ts +38 -0
  15. package/extensions/explore/ast/grammars/kotlin.wasm +0 -0
  16. package/extensions/explore/ast/grammars/manifest.json +41 -0
  17. package/extensions/explore/ast/grammars/manifest.ts +99 -0
  18. package/extensions/explore/ast/grammars/odin.wasm +0 -0
  19. package/extensions/explore/ast/grammars/swift.wasm +0 -0
  20. package/extensions/explore/ast/graph/file-graph.ts +360 -0
  21. package/extensions/explore/ast/graph/relationships.ts +597 -0
  22. package/extensions/explore/ast/identity.ts +119 -0
  23. package/extensions/explore/ast/ir.ts +136 -0
  24. package/extensions/explore/ast/languages/csharp-file-deps.ts +189 -0
  25. package/extensions/explore/ast/languages/csharp.ts +493 -0
  26. package/extensions/explore/ast/languages/file-dep-util.ts +120 -0
  27. package/extensions/explore/ast/languages/fixtures/sample.cs +17 -0
  28. package/extensions/explore/ast/languages/fixtures/sample.java +21 -0
  29. package/extensions/explore/ast/languages/fixtures/sample.kt +15 -0
  30. package/extensions/explore/ast/languages/fixtures/sample.odin +28 -0
  31. package/extensions/explore/ast/languages/fixtures/sample.rs +31 -0
  32. package/extensions/explore/ast/languages/fixtures/sample.swift +25 -0
  33. package/extensions/explore/ast/languages/go-file-deps.ts +139 -0
  34. package/extensions/explore/ast/languages/go.ts +545 -0
  35. package/extensions/explore/ast/languages/java.ts +412 -0
  36. package/extensions/explore/ast/languages/jvm-file-deps.ts +217 -0
  37. package/extensions/explore/ast/languages/kotlin.ts +611 -0
  38. package/extensions/explore/ast/languages/odin-file-deps.ts +175 -0
  39. package/extensions/explore/ast/languages/odin.ts +407 -0
  40. package/extensions/explore/ast/languages/rust-file-deps.ts +159 -0
  41. package/extensions/explore/ast/languages/rust.ts +506 -0
  42. package/extensions/explore/ast/languages/swift-file-deps.ts +59 -0
  43. package/extensions/explore/ast/languages/swift.ts +466 -0
  44. package/extensions/explore/ast/languages/tree.ts +220 -0
  45. package/extensions/explore/ast/languages/typescript-file-deps.ts +259 -0
  46. package/extensions/explore/ast/languages/typescript-package-surface.ts +440 -0
  47. package/extensions/explore/ast/languages/typescript.ts +751 -0
  48. package/extensions/explore/ast/markdown.ts +145 -0
  49. package/extensions/explore/ast/package-surface.ts +42 -0
  50. package/extensions/explore/ast/queries/ast-search.ts +306 -0
  51. package/extensions/explore/ast/queries/composite-target.ts +77 -0
  52. package/extensions/explore/ast/queries/context.ts +406 -0
  53. package/extensions/explore/ast/queries/discover.ts +427 -0
  54. package/extensions/explore/ast/queries/impact.ts +250 -0
  55. package/extensions/explore/ast/queries/outline.ts +203 -0
  56. package/extensions/explore/ast/queries/show.ts +262 -0
  57. package/extensions/explore/ast/query.ts +48 -0
  58. package/extensions/explore/ast/read/hook.ts +125 -0
  59. package/extensions/explore/ast/read/policy.ts +55 -0
  60. package/extensions/explore/ast/registry.ts +120 -0
  61. package/extensions/explore/ast/scan.ts +76 -0
  62. package/extensions/explore/ast/slice.ts +44 -0
  63. package/extensions/explore/ast/tools/ast-search.ts +138 -0
  64. package/extensions/explore/ast/tools/context.ts +126 -0
  65. package/extensions/explore/ast/tools/deps.ts +72 -0
  66. package/extensions/explore/ast/tools/discover.ts +208 -0
  67. package/extensions/explore/ast/tools/impact.ts +135 -0
  68. package/extensions/explore/ast/tools/outline.ts +200 -0
  69. package/extensions/explore/ast/tools/relationships.ts +160 -0
  70. package/extensions/explore/ast/tools/render.ts +121 -0
  71. package/extensions/explore/ast/tools/reverse-deps.ts +72 -0
  72. package/extensions/explore/ast/tools/show.ts +116 -0
  73. package/extensions/explore/guidance.ts +52 -0
  74. package/extensions/explore/index.ts +82 -92
  75. package/extensions/explore/read/autoread.ts +118 -0
  76. package/extensions/explore/settings.ts +49 -31
  77. package/extensions/explore/traverse.ts +254 -91
  78. package/extensions/handoff/index.ts +1 -1
  79. package/extensions/publish/index.ts +18 -85
  80. package/extensions/publish/publish-ui.ts +0 -4
  81. package/extensions/soul/README.md +7 -4
  82. package/extensions/soul/index.ts +11 -5
  83. package/extensions/soul/prompt.ts +31 -13
  84. package/extensions/soul/settings.ts +7 -2
  85. package/extensions/subagent/agents/context-sync.md +6 -10
  86. package/extensions/subagent/agents/review.md +24 -55
  87. package/extensions/subagent/agents/scout.md +38 -115
  88. package/extensions/subagent/run.ts +1 -1
  89. package/extensions/tau-help/help.md +2 -4
  90. package/package.json +6 -4
  91. package/schemas/tau.schema.json +35 -30
  92. package/{extensions/explore → shared}/autoread.ts +21 -7
  93. package/shared/events.ts +1 -1
  94. package/shared/full-file-knowledge.ts +49 -0
  95. package/extensions/explore/ast-guidance.ts +0 -67
  96. package/extensions/explore/ast-languages.ts +0 -61
  97. package/extensions/explore/ast-tools.ts +0 -1994
  98. package/extensions/explore/ast-worker.ts +0 -1025
  99. package/extensions/explore/find.ts +0 -147
  100. package/extensions/explore/full-file-knowledge.ts +0 -234
  101. package/extensions/explore/grep.ts +0 -749
  102. package/extensions/explore/limits.ts +0 -22
  103. package/extensions/explore/ls.ts +0 -113
  104. package/extensions/explore/orientation-state.ts +0 -280
  105. package/extensions/explore/path-display.ts +0 -41
  106. package/extensions/explore/path-tree.ts +0 -150
  107. package/extensions/explore/read-cache.ts +0 -273
  108. package/extensions/explore/read-snapshots.ts +0 -57
  109. package/extensions/explore/read-stats-panel.ts +0 -176
  110. package/extensions/explore/read-stats.ts +0 -138
  111. package/extensions/explore/read.ts +0 -372
  112. package/extensions/explore/result.ts +0 -29
  113. package/native-bin/darwin-arm64/tau-ast +0 -0
@@ -1,147 +0,0 @@
1
- import { lstat } from "node:fs/promises";
2
- import { basename, matchesGlob } from "node:path";
3
- import { defineTool } from "@earendil-works/pi-coding-agent";
4
- import { Text } from "@earendil-works/pi-tui";
5
- import { type Static, Type } from "typebox";
6
- import { formatToolRowTitle, type ToolRowStateStore } from "../../shared/tool-row-state.js";
7
- import { divideBudget, normalizeCountLimit, normalizeNonNegativeInteger } from "./limits.ts";
8
- import {
9
- formatPathForDisplay,
10
- pathResolutionError,
11
- relativeSlash,
12
- resolveExplorePath,
13
- stripLeadingAt,
14
- toSlashPath,
15
- } from "./path-display.ts";
16
- import { type PathTreeEntry, renderPathTree } from "./path-tree.ts";
17
- import { createExploreTextResult, type ExploreTextDetails, expandedExploreText } from "./result.ts";
18
- import { collectPaths, type TraversalEntry } from "./traverse.ts";
19
-
20
- const findQueryParams = Type.Object(
21
- {
22
- path: Type.Optional(Type.String()),
23
- patterns: Type.Optional(Type.Array(Type.String())),
24
- type: Type.Optional(Type.Union([Type.Literal("file"), Type.Literal("dir"), Type.Literal("any")])),
25
- maxDepth: Type.Optional(Type.Number()),
26
- hidden: Type.Optional(Type.Boolean()),
27
- noIgnore: Type.Optional(Type.Boolean()),
28
- },
29
- { additionalProperties: false },
30
- );
31
-
32
- const findParams = Type.Object(
33
- {
34
- queries: Type.Array(findQueryParams, { minItems: 1 }),
35
- limit: Type.Optional(Type.Number()),
36
- },
37
- { additionalProperties: false },
38
- );
39
-
40
- type FindParams = Static<typeof findParams>;
41
- type FindQuery = Static<typeof findQueryParams>;
42
-
43
- function globMatches(pattern: string, value: string): boolean {
44
- const normalizedValue = toSlashPath(value);
45
- const normalizedPattern = toSlashPath(pattern);
46
- return (
47
- matchesGlob(normalizedValue, normalizedPattern) ||
48
- (normalizedValue.startsWith(".") && matchesGlob(normalizedValue.slice(1), normalizedPattern))
49
- );
50
- }
51
-
52
- function queryMatches(entry: TraversalEntry, queryRoot: string, query: FindQuery): boolean {
53
- const type = query.type ?? "any";
54
- if (type !== "any" && entry.type !== type) return false;
55
- const patterns = query.patterns?.map(stripLeadingAt).filter((pattern) => pattern.length > 0) ?? [];
56
- if (patterns.length === 0) return true;
57
- const rel = relativeSlash(queryRoot, entry.absolutePath);
58
- return patterns.some((pattern) => globMatches(pattern, pattern.includes("/") ? rel : basename(entry.displayPath)));
59
- }
60
-
61
- function toTreeEntry(entry: TraversalEntry): PathTreeEntry {
62
- return { displayPath: entry.displayPath, type: entry.type };
63
- }
64
-
65
- function renderCallSummary(args: FindParams | undefined): string {
66
- const queries = args?.queries ?? [];
67
- const limit = normalizeCountLimit(args?.limit, 100);
68
- if (queries.length !== 1) return `${queries.length} queries limit=${limit}`;
69
- const query = queries[0];
70
- const path = stripLeadingAt(query?.path ?? ".");
71
- const patterns = query?.patterns?.map(stripLeadingAt).join(",") || "*";
72
- const type = query?.type ?? "any";
73
- const depth = query?.maxDepth === undefined ? "∞" : String(normalizeNonNegativeInteger(query.maxDepth, 0));
74
- const flags = [query?.hidden ? "hidden" : "", query?.noIgnore ? "noIgnore" : ""].filter(Boolean).join(" ");
75
- return `path=${path} patterns=${patterns} type=${type} depth=${depth} limit=${limit}${flags ? ` ${flags}` : ""}`;
76
- }
77
-
78
- export function createFindTool(rowState: ToolRowStateStore) {
79
- return defineTool<typeof findParams, ExploreTextDetails | undefined>({
80
- name: "find",
81
- label: "find",
82
- description: "Find files and directories by structured path queries.",
83
- promptSnippet: "Find files and directories",
84
- parameters: findParams,
85
- async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
86
- if (params.queries.length === 0) throw new Error("find requires at least one query");
87
- const limit = normalizeCountLimit(params.limit, 100);
88
- const budgets = divideBudget(limit, params.queries.length);
89
- const humanParts: string[] = [];
90
- const agentParts: string[] = [];
91
-
92
- for (let i = 0; i < params.queries.length; i += 1) {
93
- const query = params.queries[i];
94
- if (!query) continue;
95
- const inputPath = query.path ?? ".";
96
- const root = resolveExplorePath(ctx.cwd, inputPath);
97
- const rootStats = await lstat(root).catch((error: unknown) => {
98
- throw pathResolutionError(error, inputPath);
99
- });
100
- const entries = await collectPaths({
101
- cwd: ctx.cwd,
102
- root,
103
- maxDepth: query.maxDepth === undefined ? undefined : normalizeNonNegativeInteger(query.maxDepth, 0),
104
- includeRoot: rootStats.isFile(),
105
- includeHidden: query.hidden === true,
106
- includeIgnored: query.noIgnore === true,
107
- includeNoise: query.noIgnore === true,
108
- }).catch((error: unknown) => {
109
- throw new Error(`Query ${i + 1} failed: ${error instanceof Error ? error.message : String(error)}`);
110
- });
111
- const matches = entries.filter((entry) => queryMatches(entry, root, query));
112
- const budget = budgets[i] ?? limit;
113
- const shown = matches.slice(0, budget);
114
- const omitted = matches.length - shown.length;
115
- const notice = omitted > 0 ? `… omitted ${omitted} matches (limit ${budget})` : undefined;
116
- const rootPath = formatPathForDisplay(root, ctx.cwd);
117
- const tree =
118
- shown.length > 0
119
- ? renderPathTree(shown.map(toTreeEntry), { rootPath, omissionNotice: notice })
120
- : undefined;
121
- const humanText = tree?.humanText ?? "No matches";
122
- const agentText = tree?.agentText ?? "No matches";
123
- if (params.queries.length === 1) {
124
- humanParts.push(humanText);
125
- agentParts.push(agentText);
126
- } else {
127
- humanParts.push(`query ${i + 1}\n${humanText}`);
128
- agentParts.push(`q${i + 1}\n${agentText}`);
129
- }
130
- }
131
-
132
- return createExploreTextResult(agentParts.join("\n"), humanParts.join("\n"));
133
- },
134
- renderCall(args, theme, context) {
135
- rowState.watch(context.toolCallId, context.invalidate);
136
- const text = (context.lastComponent as Text | undefined) ?? new Text("", 0, 0);
137
- const title = formatToolRowTitle(rowState, context.toolCallId, "find", theme);
138
- text.setText(`${title} ${theme.fg("muted", renderCallSummary(args))}`);
139
- return text;
140
- },
141
- renderResult(result, _options, _theme, context) {
142
- const text = (context.lastComponent as Text | undefined) ?? new Text("", 0, 0);
143
- text.setText(context.expanded ? expandedExploreText(result) : "");
144
- return text;
145
- },
146
- });
147
- }
@@ -1,234 +0,0 @@
1
- import { createHash } from "node:crypto";
2
- import { generateUnifiedPatch, truncateHead } from "@earendil-works/pi-coding-agent";
3
- import type { ReadCacheMetaV1, ReadCacheMode, ReadCachePresentation } from "./read-cache.ts";
4
-
5
- export const COMPLETE_FILE_SCOPE = "full";
6
- export const MAX_COMPLETE_FILE_SNAPSHOT_BYTES = 1024 * 1024;
7
-
8
- export interface CompleteFileSelection {
9
- text: string;
10
- mode: ReadCacheMode;
11
- summary: string;
12
- }
13
-
14
- export function createCompleteFileMeta(options: {
15
- pathKey: string;
16
- presentation: ReadCachePresentation;
17
- servedHash: string;
18
- mode: ReadCacheMode;
19
- sourceText: string;
20
- returnedText: string;
21
- totalLines: number;
22
- summary: string;
23
- baseHash?: string;
24
- }): ReadCacheMetaV1 {
25
- return {
26
- v: 1,
27
- pathKey: options.pathKey,
28
- scopeKey: COMPLETE_FILE_SCOPE,
29
- presentation: options.presentation,
30
- servedHash: options.servedHash,
31
- baseHash: options.baseHash,
32
- mode: options.mode,
33
- baselineTokens: estimateTokens(options.sourceText),
34
- returnedTokens: estimateTokens(options.returnedText),
35
- totalLines: options.totalLines,
36
- summary: options.summary,
37
- };
38
- }
39
-
40
- export function readScopeKey(
41
- completeFile: boolean,
42
- startLine: number,
43
- endLine: number,
44
- presentation: ReadCachePresentation,
45
- ): string {
46
- return completeFile ? COMPLETE_FILE_SCOPE : `r:${startLine}:${endLine}:n${presentation === "line-numbered" ? 1 : 0}`;
47
- }
48
-
49
- export function decodeCompleteFileSource(options: {
50
- content: unknown;
51
- autoread: boolean;
52
- pathHeader?: string;
53
- presentation: ReadCachePresentation;
54
- servedHash: string;
55
- }): { valid: boolean; text?: string } {
56
- const payload = textContent(options.content);
57
- if (payload === undefined) return { valid: false };
58
- const separator = payload.indexOf("\n");
59
- if (
60
- options.autoread &&
61
- (separator < 0 || options.pathHeader === undefined || payload.slice(0, separator) !== options.pathHeader)
62
- )
63
- return { valid: false };
64
- const body = options.autoread ? payload.slice(separator + 1) : payload;
65
- const text = options.presentation === "plain" ? body : decodeLineNumbers(body);
66
- if (text === undefined) return { valid: false };
67
- const bytes = Buffer.byteLength(text, "utf8");
68
- if (createHash("sha256").update(text, "utf8").digest("hex") !== options.servedHash) return { valid: false };
69
- return bytes <= MAX_COMPLETE_FILE_SNAPSHOT_BYTES ? { valid: true, text } : { valid: true };
70
- }
71
-
72
- export function applyCompleteFileDiff(options: {
73
- content: unknown;
74
- baseText: string;
75
- baseHash: string;
76
- servedHash: string;
77
- }): { valid: boolean; text?: string } {
78
- if (hashText(options.baseText) !== options.baseHash) return { valid: false };
79
- const payload = textContent(options.content);
80
- if (payload === undefined) return { valid: false };
81
- const separator = payload.indexOf("\n");
82
- if (separator < 0 || !/^\[read: \d+ lines added, \d+ removed of \d+\]$/.test(payload.slice(0, separator))) {
83
- return { valid: false };
84
- }
85
- const text = applyUnifiedDiff(options.baseText, payload.slice(separator + 1));
86
- if (text === undefined || hashText(text) !== options.servedHash) return { valid: false };
87
- return Buffer.byteLength(text, "utf8") <= MAX_COMPLETE_FILE_SNAPSHOT_BYTES ? { valid: true, text } : { valid: true };
88
- }
89
-
90
- export function selectCompleteFileResponse(options: {
91
- displayPath: string;
92
- currentText: string;
93
- currentHash: string;
94
- fullText: string;
95
- totalLines: number;
96
- recovery: boolean;
97
- baseHash?: string;
98
- baselineText?: string;
99
- }): CompleteFileSelection {
100
- if (options.recovery) return { text: options.fullText, mode: "recovery", summary: `${options.totalLines} lines` };
101
- if (!options.baseHash) return { text: options.fullText, mode: "baseline", summary: `${options.totalLines} lines` };
102
- if (options.baseHash === options.currentHash) {
103
- const text = `unchanged, ${options.totalLines} lines`;
104
- return { text, mode: "unchanged", summary: text };
105
- }
106
- if (options.baselineText !== undefined) {
107
- const patch = generateUnifiedPatch(options.displayPath, options.baselineText, options.currentText, 3);
108
- const counts = countDiffLines(patch);
109
- const candidate = `[read: ${counts.added} lines added, ${counts.removed} removed of ${options.totalLines}]\n${patch}`;
110
- if (!truncateHead(candidate).truncated && estimateTokens(candidate) < estimateTokens(options.fullText)) {
111
- return { text: candidate, mode: "diff", summary: `+${counts.added} -${counts.removed}` };
112
- }
113
- }
114
- return { text: options.fullText, mode: "baseline", summary: `${options.totalLines} lines` };
115
- }
116
-
117
- export function estimateTokens(text: string): number {
118
- return Math.ceil(text.length / 4);
119
- }
120
-
121
- function textContent(content: unknown): string | undefined {
122
- if (typeof content === "string") return content;
123
- if (!Array.isArray(content) || content.length !== 1) return undefined;
124
- const part = content[0];
125
- if (!part || typeof part !== "object") return undefined;
126
- const record = part as Record<string, unknown>;
127
- return record.type === "text" && typeof record.text === "string" ? record.text : undefined;
128
- }
129
-
130
- function applyUnifiedDiff(source: string, patch: string): string | undefined {
131
- const patchLines = patch.split("\n");
132
- const oldHeaderIndex = patchLines.findIndex(
133
- (line, index) => line.startsWith("--- ") && patchLines[index + 1]?.startsWith("+++ "),
134
- );
135
- if (oldHeaderIndex < 0) return undefined;
136
- const sourceEndsWithNewline = source.endsWith("\n");
137
- const sourceLines = source === "" ? [] : source.split("\n");
138
- if (sourceEndsWithNewline) sourceLines.pop();
139
- const output: string[] = [];
140
- let sourceIndex = 0;
141
- let patchIndex = oldHeaderIndex + 2;
142
- let hunkCount = 0;
143
- let outputEndsWithNewline = sourceEndsWithNewline;
144
-
145
- while (patchIndex < patchLines.length) {
146
- const header = patchLines[patchIndex];
147
- if (header === "" && patchIndex === patchLines.length - 1) break;
148
- if (header === undefined) return undefined;
149
- const match = /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@(?: .*)?$/.exec(header);
150
- if (!match) return undefined;
151
- const oldStartValue = Number(match[1]);
152
- const oldCount = match[2] === undefined ? 1 : Number(match[2]);
153
- const newCount = match[4] === undefined ? 1 : Number(match[4]);
154
- if (!Number.isSafeInteger(oldStartValue) || !Number.isSafeInteger(oldCount) || !Number.isSafeInteger(newCount)) {
155
- return undefined;
156
- }
157
- const oldStart = oldCount === 0 ? oldStartValue : oldStartValue - 1;
158
- if (oldStart < sourceIndex || oldStart > sourceLines.length) return undefined;
159
- output.push(...sourceLines.slice(sourceIndex, oldStart));
160
- sourceIndex = oldStart;
161
- patchIndex += 1;
162
- let consumedOld = 0;
163
- let producedNew = 0;
164
- let previousMarker: string | undefined;
165
- let newSideHasNoFinalNewline = false;
166
-
167
- while (patchIndex < patchLines.length && !patchLines[patchIndex]?.startsWith("@@ ")) {
168
- const line = patchLines[patchIndex];
169
- if (line === "" && patchIndex === patchLines.length - 1) break;
170
- if (line === undefined) return undefined;
171
- if (line === "\") {
172
- if (previousMarker !== "+" && previousMarker !== "-" && previousMarker !== " ") return undefined;
173
- if (previousMarker === "+" || previousMarker === " ") newSideHasNoFinalNewline = true;
174
- patchIndex += 1;
175
- continue;
176
- }
177
- const marker = line[0];
178
- const value = line.slice(1);
179
- if (marker === " ") {
180
- if (sourceLines[sourceIndex] !== value) return undefined;
181
- output.push(value);
182
- sourceIndex += 1;
183
- consumedOld += 1;
184
- producedNew += 1;
185
- } else if (marker === "-") {
186
- if (sourceLines[sourceIndex] !== value) return undefined;
187
- sourceIndex += 1;
188
- consumedOld += 1;
189
- } else if (marker === "+") {
190
- output.push(value);
191
- producedNew += 1;
192
- } else {
193
- return undefined;
194
- }
195
- previousMarker = marker;
196
- patchIndex += 1;
197
- }
198
- if (consumedOld !== oldCount || producedNew !== newCount) return undefined;
199
- if (sourceIndex === sourceLines.length) {
200
- outputEndsWithNewline = newCount > 0 && !newSideHasNoFinalNewline;
201
- }
202
- hunkCount += 1;
203
- }
204
- if (hunkCount === 0) return undefined;
205
- output.push(...sourceLines.slice(sourceIndex));
206
- const text = output.join("\n");
207
- return outputEndsWithNewline && output.length > 0 ? `${text}\n` : text;
208
- }
209
-
210
- function hashText(text: string): string {
211
- return createHash("sha256").update(text, "utf8").digest("hex");
212
- }
213
-
214
- function decodeLineNumbers(content: string): string | undefined {
215
- const lines = content.split("\n");
216
- const decoded: string[] = [];
217
- for (let index = 0; index < lines.length; index += 1) {
218
- const prefix = `${index + 1}: `;
219
- const line = lines[index];
220
- if (line === undefined || !line.startsWith(prefix)) return undefined;
221
- decoded.push(line.slice(prefix.length));
222
- }
223
- return decoded.join("\n");
224
- }
225
-
226
- function countDiffLines(patch: string): { added: number; removed: number } {
227
- let added = 0;
228
- let removed = 0;
229
- for (const line of patch.split("\n")) {
230
- if (line.startsWith("+") && !line.startsWith("+++")) added += 1;
231
- else if (line.startsWith("-") && !line.startsWith("---")) removed += 1;
232
- }
233
- return { added, removed };
234
- }