@shanepadgett/tau-agent 0.27.0 → 0.28.0

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 +130 -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 +716 -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 +31 -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,22 +0,0 @@
1
- export function normalizeNonNegativeInteger(value: number | undefined, fallback: number): number {
2
- if (value === undefined) return fallback;
3
- if (!Number.isFinite(value)) return fallback;
4
- return Math.max(0, Math.floor(value));
5
- }
6
-
7
- export function normalizeCountLimit(value: number | undefined, fallback: number): number {
8
- if (value === undefined) return fallback;
9
- if (!Number.isFinite(value)) return fallback;
10
- return Math.max(1, Math.floor(value));
11
- }
12
-
13
- export function divideBudget(limit: number, count: number): number[] {
14
- if (count <= 0) return [];
15
- const base = Math.max(1, Math.floor(limit / count));
16
- let remainder = Math.max(0, limit - base * count);
17
- return Array.from({ length: count }, () => {
18
- const extra = remainder > 0 ? 1 : 0;
19
- if (remainder > 0) remainder -= 1;
20
- return base + extra;
21
- });
22
- }
@@ -1,113 +0,0 @@
1
- import { defineTool, formatSize } from "@earendil-works/pi-coding-agent";
2
- import { Text } from "@earendil-works/pi-tui";
3
- import { type Static, Type } from "typebox";
4
- import { formatToolRowTitle, type ToolRowStateStore } from "../../shared/tool-row-state.js";
5
- import { divideBudget, normalizeCountLimit, normalizeNonNegativeInteger } from "./limits.ts";
6
- import { formatPathForDisplay, pathResolutionError, resolveExplorePath, stripLeadingAt } from "./path-display.ts";
7
- import { type PathTreeEntry, renderPathTree } from "./path-tree.ts";
8
- import { createExploreTextResult, type ExploreTextDetails, expandedExploreText } from "./result.ts";
9
- import { collectPaths, type TraversalEntry } from "./traverse.ts";
10
-
11
- const lsParams = Type.Object(
12
- {
13
- paths: Type.Optional(Type.Array(Type.String())),
14
- depth: Type.Optional(Type.Number()),
15
- limit: Type.Optional(Type.Number()),
16
- all: Type.Optional(Type.Boolean()),
17
- long: Type.Optional(Type.Boolean()),
18
- },
19
- { additionalProperties: false },
20
- );
21
-
22
- type LsParams = Static<typeof lsParams>;
23
-
24
- function formatTime(date: Date): string {
25
- return date.toISOString().slice(0, 10);
26
- }
27
-
28
- function entryMetadata(entry: TraversalEntry, long: boolean): string | undefined {
29
- if (!long) return undefined;
30
- const size = entry.type === "dir" ? "dir" : formatSize(entry.stats.size);
31
- return `${size} ${formatTime(entry.stats.mtime)}`;
32
- }
33
-
34
- function pathTreeEntry(entry: TraversalEntry, long: boolean): PathTreeEntry {
35
- return {
36
- displayPath: entry.displayPath,
37
- type: entry.type,
38
- metadata: entryMetadata(entry, long),
39
- empty: entry.empty,
40
- };
41
- }
42
-
43
- function renderCallSummary(args: LsParams | undefined): string {
44
- const paths = args?.paths && args.paths.length > 0 ? args.paths.map(stripLeadingAt).join(" ") : ".";
45
- const depth = normalizeNonNegativeInteger(args?.depth, 1);
46
- const limit = normalizeCountLimit(args?.limit, 100);
47
- const flags = [args?.all ? "all" : "", args?.long ? "long" : ""].filter(Boolean).join(" ");
48
- return `paths=${paths} depth=${depth} limit=${limit}${flags ? ` ${flags}` : ""}`;
49
- }
50
-
51
- export function createLsTool(rowState: ToolRowStateStore) {
52
- return defineTool<typeof lsParams, ExploreTextDetails | undefined>({
53
- name: "ls",
54
- label: "ls",
55
- description: "List files and directories with compact path output.",
56
- promptSnippet: "List files and directories",
57
- parameters: lsParams,
58
- async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
59
- const requestedPaths = params.paths && params.paths.length > 0 ? params.paths : ["."];
60
- const depth = normalizeNonNegativeInteger(params.depth, 1);
61
- const limit = normalizeCountLimit(params.limit, 100);
62
- const budgets = divideBudget(limit, requestedPaths.length);
63
- const all = params.all === true;
64
- const humanParts: string[] = [];
65
- const agentParts: string[] = [];
66
-
67
- for (let i = 0; i < requestedPaths.length; i += 1) {
68
- const input = requestedPaths[i] ?? ".";
69
- const root = resolveExplorePath(ctx.cwd, input);
70
- const entries = await collectPaths({
71
- cwd: ctx.cwd,
72
- root,
73
- maxDepth: depth,
74
- includeRoot: true,
75
- includeHidden: all,
76
- includeIgnored: all,
77
- includeNoise: all,
78
- }).catch((error: unknown) => {
79
- throw pathResolutionError(error, input);
80
- });
81
-
82
- const budget = budgets[i] ?? limit;
83
- const shown = entries.slice(0, budget);
84
- const omitted = entries.length - shown.length;
85
- const notice = omitted > 0 ? `… omitted ${omitted} entries (limit ${budget})` : undefined;
86
- const rootPath = formatPathForDisplay(root, ctx.cwd);
87
- const tree = renderPathTree(
88
- shown.map((entry) => pathTreeEntry(entry, params.long === true)),
89
- {
90
- rootPath,
91
- omissionNotice: notice,
92
- },
93
- );
94
- humanParts.push(tree.humanText);
95
- agentParts.push(tree.agentText);
96
- }
97
-
98
- return createExploreTextResult(agentParts.join("\n\n"), humanParts.join("\n\n"));
99
- },
100
- renderCall(args, theme, context) {
101
- rowState.watch(context.toolCallId, context.invalidate);
102
- const text = (context.lastComponent as Text | undefined) ?? new Text("", 0, 0);
103
- const title = formatToolRowTitle(rowState, context.toolCallId, "ls", theme);
104
- text.setText(`${title} ${theme.fg("muted", renderCallSummary(args))}`);
105
- return text;
106
- },
107
- renderResult(result, _options, _theme, context) {
108
- const text = (context.lastComponent as Text | undefined) ?? new Text("", 0, 0);
109
- text.setText(context.expanded ? expandedExploreText(result) : "");
110
- return text;
111
- },
112
- });
113
- }
@@ -1,280 +0,0 @@
1
- import { createHash } from "node:crypto";
2
- import { realpathSync } from "node:fs";
3
- import { resolve } from "node:path";
4
- import { astLanguageForPath, type AstLanguage } from "./ast-languages.ts";
5
-
6
- export type StructuralAttemptKind =
7
- | "directOutline"
8
- | "symbol"
9
- | "apiCandidate"
10
- | "structuralMatch"
11
- | "relationshipLocation"
12
- | "relationshipScope";
13
- export type ReadGateDecision = StructuralAttemptKind | "fatalFallback" | "blocked";
14
- export type ReadPermission = Exclude<ReadGateDecision, "blocked"> | "postPatchDiff" | "ungated";
15
-
16
- export interface OrientationTelemetry {
17
- blockedReadAttempts: number;
18
- permittedReadAttempts: number;
19
- fallbackReadAttempts: number;
20
- permissionReadAttempts: Record<Exclude<ReadPermission, "ungated">, number>;
21
- workerInputBytes: number;
22
- completeRenderedBytes: number;
23
- modelVisibleAstBytes: number;
24
- sourceBytesDeflected: number;
25
- temporaryOutputBytes: number;
26
- directReadBytes: number;
27
- overflowReadBytes: number;
28
- }
29
-
30
- interface AttemptRecord {
31
- fingerprint: string;
32
- kind: StructuralAttemptKind;
33
- }
34
-
35
- interface FatalFallbackRecord {
36
- fingerprint: string;
37
- includePrivate: boolean;
38
- names: readonly string[];
39
- diagnostic: {
40
- code: "outline_failed" | "outlineFailed" | "response_too_large" | "resultFrameTooLarge";
41
- message: string;
42
- };
43
- }
44
-
45
- interface FileState {
46
- attempt: AttemptRecord | undefined;
47
- fallback: FatalFallbackRecord | undefined;
48
- patchFingerprint: string | undefined;
49
- }
50
-
51
- type TelemetryEvent =
52
- | {
53
- toolCallId: string;
54
- kind: "outline";
55
- workerInputBytes: number;
56
- completeRenderedBytes: number;
57
- modelVisibleAstBytes: number;
58
- temporaryOutputBytes: number;
59
- }
60
- | {
61
- toolCallId: string;
62
- kind: "structuralAttempt";
63
- sourceBytesDeflected: number;
64
- }
65
- | {
66
- toolCallId: string;
67
- kind: "blockedRead";
68
- }
69
- | {
70
- toolCallId: string;
71
- kind: "read";
72
- permission: ReadPermission;
73
- returnedBytes: number;
74
- overflow: boolean;
75
- };
76
-
77
- export interface OrientationState {
78
- supports(path: string): Promise<boolean>;
79
- check(path: string, fingerprint: string): ReadGateDecision;
80
- checkPatch(path: string, fingerprint: string): boolean;
81
- recordAttempts(
82
- records: readonly {
83
- path: string;
84
- fingerprint: string;
85
- kind: StructuralAttemptKind;
86
- toolCallId: string;
87
- sourceBytesDeflected: number;
88
- }[],
89
- ): void;
90
- recordFatal(input: {
91
- path: string;
92
- fingerprint: string;
93
- includePrivate: boolean;
94
- names: readonly string[];
95
- code: "outline_failed" | "outlineFailed" | "response_too_large" | "resultFrameTooLarge";
96
- message: string;
97
- }): void;
98
- recordPatched(records: readonly { path: string; resultingFingerprint: string | null }[]): void;
99
- recordOutlineTelemetry(
100
- toolCallId: string,
101
- metrics: {
102
- workerInputBytes: number;
103
- completeRenderedBytes: number;
104
- modelVisibleAstBytes: number;
105
- temporaryOutputBytes: number;
106
- },
107
- ): void;
108
- recordBlockedRead(toolCallId: string, path: string): void;
109
- recordRead(toolCallId: string, path: string, permission: ReadPermission, returnedBytes: number): void;
110
- recordTemporaryOutput(path: string): void;
111
- telemetry(includedToolCallIds: ReadonlySet<string> | undefined): OrientationTelemetry;
112
- invalidate(paths: readonly string[]): void;
113
- clear(): void;
114
- resetGate(): void;
115
- }
116
-
117
- export function sourceFingerprint(source: Uint8Array): string {
118
- return `sha256:${createHash("sha256").update(source).digest("hex")}`;
119
- }
120
-
121
- export function createOrientationState(workerLanguages: () => Promise<readonly AstLanguage[]>): OrientationState {
122
- const files = new Map<string, FileState>();
123
- const temporaryOutputs = new Set<string>();
124
- const events: TelemetryEvent[] = [];
125
-
126
- function stateFor(path: string): FileState {
127
- const canonicalPath = canonicalPathForState(path);
128
- const existing = files.get(canonicalPath);
129
- if (existing) return existing;
130
- const state: FileState = { attempt: undefined, fallback: undefined, patchFingerprint: undefined };
131
- files.set(canonicalPath, state);
132
- return state;
133
- }
134
-
135
- return {
136
- async supports(path) {
137
- const language = astLanguageForPath(canonicalPathForState(path));
138
- if (!language) return false;
139
- try {
140
- return (await workerLanguages()).includes(language);
141
- } catch {
142
- return false;
143
- }
144
- },
145
- check(path, fingerprint) {
146
- const state = files.get(canonicalPathForState(path));
147
- if (!state) return "blocked";
148
- if (state.attempt && state.attempt.fingerprint !== fingerprint) state.attempt = undefined;
149
- if (state.fallback && state.fallback.fingerprint !== fingerprint) state.fallback = undefined;
150
- if (state.attempt?.fingerprint === fingerprint) return state.attempt.kind;
151
- return state.fallback?.fingerprint === fingerprint ? "fatalFallback" : "blocked";
152
- },
153
- checkPatch(path, fingerprint) {
154
- const state = files.get(canonicalPathForState(path));
155
- if (!state?.patchFingerprint) return false;
156
- if (state.patchFingerprint === fingerprint) return true;
157
- state.patchFingerprint = undefined;
158
- return false;
159
- },
160
- recordAttempts(records) {
161
- for (const record of records) {
162
- const state = stateFor(record.path);
163
- state.attempt = { fingerprint: record.fingerprint, kind: record.kind };
164
- state.fallback = undefined;
165
- events.push({
166
- toolCallId: record.toolCallId,
167
- kind: "structuralAttempt",
168
- sourceBytesDeflected: record.sourceBytesDeflected,
169
- });
170
- }
171
- },
172
- recordFatal(input) {
173
- const state = stateFor(input.path);
174
- state.fallback = {
175
- fingerprint: input.fingerprint,
176
- includePrivate: input.includePrivate,
177
- names: [...input.names],
178
- diagnostic: { code: input.code, message: input.message },
179
- };
180
- },
181
- recordPatched(records) {
182
- for (const record of records) {
183
- if (record.resultingFingerprint === null) continue;
184
- stateFor(record.path).patchFingerprint = record.resultingFingerprint;
185
- }
186
- },
187
- recordOutlineTelemetry(toolCallId, metrics) {
188
- events.push({ toolCallId, kind: "outline", ...metrics });
189
- },
190
- recordBlockedRead(toolCallId, path) {
191
- stateFor(path);
192
- events.push({ toolCallId, kind: "blockedRead" });
193
- },
194
- recordRead(toolCallId, path, permission, returnedBytes) {
195
- events.push({
196
- toolCallId,
197
- kind: "read",
198
- permission,
199
- returnedBytes,
200
- overflow: temporaryOutputs.has(canonicalPathForState(path)),
201
- });
202
- },
203
- recordTemporaryOutput(path) {
204
- temporaryOutputs.add(canonicalPathForState(path));
205
- },
206
- telemetry(includedToolCallIds) {
207
- const permissionReadAttempts: OrientationTelemetry["permissionReadAttempts"] = {
208
- directOutline: 0,
209
- symbol: 0,
210
- apiCandidate: 0,
211
- structuralMatch: 0,
212
- relationshipLocation: 0,
213
- relationshipScope: 0,
214
- fatalFallback: 0,
215
- postPatchDiff: 0,
216
- };
217
- const result: OrientationTelemetry = {
218
- blockedReadAttempts: 0,
219
- permittedReadAttempts: 0,
220
- fallbackReadAttempts: 0,
221
- permissionReadAttempts,
222
- workerInputBytes: 0,
223
- completeRenderedBytes: 0,
224
- modelVisibleAstBytes: 0,
225
- sourceBytesDeflected: 0,
226
- temporaryOutputBytes: 0,
227
- directReadBytes: 0,
228
- overflowReadBytes: 0,
229
- };
230
- for (const event of events) {
231
- if (includedToolCallIds && !includedToolCallIds.has(event.toolCallId)) continue;
232
- if (event.kind === "outline") {
233
- result.workerInputBytes += event.workerInputBytes;
234
- result.completeRenderedBytes += event.completeRenderedBytes;
235
- result.modelVisibleAstBytes += event.modelVisibleAstBytes;
236
- result.temporaryOutputBytes += event.temporaryOutputBytes;
237
- } else if (event.kind === "structuralAttempt") {
238
- result.sourceBytesDeflected += event.sourceBytesDeflected;
239
- } else if (event.kind === "blockedRead") {
240
- result.blockedReadAttempts += 1;
241
- } else {
242
- result.directReadBytes += event.returnedBytes;
243
- if (event.overflow) result.overflowReadBytes += event.returnedBytes;
244
- if (event.permission !== "ungated") {
245
- result.permittedReadAttempts += 1;
246
- result.permissionReadAttempts[event.permission] += 1;
247
- }
248
- if (event.permission === "fatalFallback") result.fallbackReadAttempts += 1;
249
- }
250
- }
251
- return result;
252
- },
253
- invalidate(paths) {
254
- for (const path of paths) {
255
- const state = files.get(canonicalPathForState(path));
256
- if (!state) continue;
257
- state.attempt = undefined;
258
- state.fallback = undefined;
259
- state.patchFingerprint = undefined;
260
- }
261
- },
262
- clear() {
263
- files.clear();
264
- temporaryOutputs.clear();
265
- events.length = 0;
266
- },
267
- resetGate() {
268
- files.clear();
269
- },
270
- };
271
- }
272
-
273
- function canonicalPathForState(path: string): string {
274
- const absolutePath = resolve(path);
275
- try {
276
- return realpathSync(absolutePath);
277
- } catch {
278
- return absolutePath;
279
- }
280
- }
@@ -1,41 +0,0 @@
1
- import { isAbsolute, relative, resolve } from "node:path";
2
-
3
- export function stripLeadingAt(value: string): string {
4
- return value.startsWith("@") ? value.slice(1) : value;
5
- }
6
-
7
- export function toSlashPath(value: string): string {
8
- return value.replace(/\\/g, "/");
9
- }
10
-
11
- export function resolveExplorePath(cwd: string, input: string): string {
12
- const path = stripLeadingAt(input);
13
- if (path.length === 0) return resolve(cwd);
14
- return isAbsolute(path) ? resolve(path) : resolve(cwd, path);
15
- }
16
-
17
- export function pathResolutionError(error: unknown, input: string): Error {
18
- if (error instanceof Error && "code" in error && error.code === "ENOENT") {
19
- return new Error(`Path not found: ${stripLeadingAt(input)}`);
20
- }
21
- return error instanceof Error ? error : new Error(String(error));
22
- }
23
-
24
- export function isWithinPath(parent: string, child: string): boolean {
25
- const rel = relative(resolve(parent), resolve(child));
26
- return rel === "" || (!rel.startsWith("..") && !isAbsolute(rel));
27
- }
28
-
29
- export function formatPathForDisplay(absolutePath: string, cwd: string): string {
30
- const resolvedCwd = resolve(cwd);
31
- const resolvedPath = resolve(absolutePath);
32
- const rel = relative(resolvedCwd, resolvedPath);
33
- if (rel === "") return ".";
34
- if (!rel.startsWith("..") && !isAbsolute(rel)) return toSlashPath(rel);
35
- return toSlashPath(resolvedPath);
36
- }
37
-
38
- export function relativeSlash(from: string, to: string): string {
39
- const rel = relative(resolve(from), resolve(to));
40
- return rel === "" ? "." : toSlashPath(rel);
41
- }
@@ -1,150 +0,0 @@
1
- export type PathTreeEntryType = "file" | "dir";
2
-
3
- export interface PathTreeEntry {
4
- displayPath: string;
5
- type: PathTreeEntryType;
6
- metadata?: string;
7
- empty?: boolean;
8
- }
9
-
10
- export interface PathTreeRenderOptions {
11
- rootPath?: string;
12
- omissionNotice?: string;
13
- }
14
-
15
- export interface PathTreeRender {
16
- humanText: string;
17
- agentText: string;
18
- }
19
-
20
- interface TreeNode {
21
- name: string;
22
- type: PathTreeEntryType;
23
- metadata?: string;
24
- empty?: boolean;
25
- children: Map<string, TreeNode>;
26
- }
27
-
28
- function splitDisplayPath(displayPath: string): string[] {
29
- if (displayPath === ".") return [];
30
- if (displayPath === "/") return ["/"];
31
- if (displayPath.startsWith("/")) return ["/", ...displayPath.slice(1).split("/").filter(Boolean)];
32
- return displayPath.split("/").filter(Boolean);
33
- }
34
-
35
- function isUnderRoot(rootPath: string, displayPath: string): boolean {
36
- if (rootPath === "/") return displayPath.startsWith("/");
37
- if (rootPath === ".") return !displayPath.startsWith("/");
38
- return displayPath === rootPath || displayPath.startsWith(`${rootPath}/`);
39
- }
40
-
41
- function relativeToRoot(rootPath: string, displayPath: string): string {
42
- if (rootPath === "/") return displayPath === "/" ? "" : displayPath.slice(1);
43
- if (rootPath === ".") return displayPath === "." ? "" : displayPath;
44
- if (displayPath === rootPath) return "";
45
- return displayPath.slice(rootPath.length + 1);
46
- }
47
-
48
- function entryLabel(node: TreeNode): string {
49
- const name = node.name === "/" ? "/" : node.type === "dir" ? `${node.name}/` : node.name;
50
- return node.metadata ? `${name} ${node.metadata}` : name;
51
- }
52
-
53
- function sortChildren(node: TreeNode): TreeNode[] {
54
- return [...node.children.values()].sort((a, b) => {
55
- if (a.type !== b.type) return a.type === "dir" ? -1 : 1;
56
- return a.name.localeCompare(b.name);
57
- });
58
- }
59
-
60
- function createNode(name: string, type: PathTreeEntryType): TreeNode {
61
- return { name, type, children: new Map() };
62
- }
63
-
64
- function insert(root: TreeNode, relativePath: string, entry: PathTreeEntry): void {
65
- const parts = splitDisplayPath(relativePath);
66
- if (parts.length === 0) {
67
- root.type = entry.type;
68
- root.metadata = entry.metadata;
69
- root.empty = entry.empty;
70
- return;
71
- }
72
-
73
- let current = root;
74
- for (let i = 0; i < parts.length; i += 1) {
75
- const part = parts[i];
76
- if (!part) continue;
77
- const isLeaf = i === parts.length - 1;
78
- const type = isLeaf ? entry.type : "dir";
79
- let child = current.children.get(part);
80
- if (!child) {
81
- child = createNode(part, type);
82
- current.children.set(part, child);
83
- }
84
- if (isLeaf) {
85
- child.type = entry.type;
86
- child.metadata = entry.metadata;
87
- child.empty = entry.empty;
88
- }
89
- current = child;
90
- }
91
- }
92
-
93
- function renderHumanNode(node: TreeNode, depth: number, lines: string[]): void {
94
- lines.push(`${" ".repeat(depth)}${entryLabel(node)}`);
95
- if (node.empty && node.children.size === 0) lines.push(`${" ".repeat(depth + 1)}[empty]`);
96
- for (const child of sortChildren(node)) renderHumanNode(child, depth + 1, lines);
97
- }
98
-
99
- function renderAgentNode(node: TreeNode, depth: number, lines: string[]): void {
100
- if (node.type === "file") {
101
- lines.push(`${" ".repeat(depth)}${entryLabel(node)}`);
102
- return;
103
- }
104
-
105
- const children = sortChildren(node);
106
- const files = children.filter((child) => child.type === "file").map(entryLabel);
107
- let line = `${" ".repeat(depth)}${entryLabel(node)}`;
108
- if (files.length > 0) line += `: ${files.join(", ")}`;
109
- else if (node.empty && children.length === 0) line += " [empty]";
110
- lines.push(line);
111
-
112
- for (const child of children) {
113
- if (child.type === "dir") renderAgentNode(child, depth + 1, lines);
114
- }
115
- }
116
-
117
- function buildTree(entries: readonly PathTreeEntry[], rootPath: string | undefined): TreeNode {
118
- const rootName = rootPath === undefined ? "" : rootPath === "." ? "." : rootPath;
119
- const root = createNode(rootName, "dir");
120
- for (const entry of entries) {
121
- if (rootPath !== undefined && isUnderRoot(rootPath, entry.displayPath)) {
122
- insert(root, relativeToRoot(rootPath, entry.displayPath), entry);
123
- } else {
124
- insert(root, entry.displayPath, entry);
125
- }
126
- }
127
- return root;
128
- }
129
-
130
- function appendNotice(lines: string[], notice: string | undefined): void {
131
- if (notice) lines.push(notice);
132
- }
133
-
134
- export function renderPathTree(entries: readonly PathTreeEntry[], options: PathTreeRenderOptions = {}): PathTreeRender {
135
- const root = buildTree(entries, options.rootPath);
136
- const humanLines: string[] = [];
137
- const agentLines: string[] = [];
138
-
139
- if (options.rootPath === undefined) {
140
- for (const child of sortChildren(root)) renderHumanNode(child, 0, humanLines);
141
- for (const child of sortChildren(root)) renderAgentNode(child, 0, agentLines);
142
- } else {
143
- renderHumanNode(root, 0, humanLines);
144
- renderAgentNode(root, 0, agentLines);
145
- }
146
-
147
- appendNotice(humanLines, options.omissionNotice);
148
- appendNotice(agentLines, options.omissionNotice);
149
- return { humanText: humanLines.join("\n"), agentText: agentLines.join("\n") };
150
- }