@shanepadgett/tau-agent 0.26.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.
- package/docs/subagents.md +1 -1
- package/extensions/appshot/README.md +1 -1
- package/extensions/appshot/index.ts +5 -11
- package/extensions/context/sync.ts +1 -9
- package/extensions/context-pruning/README.md +2 -2
- package/extensions/context-pruning/index.ts +58 -57
- package/extensions/context-pruning/prune.ts +2 -2
- package/extensions/context-pruning/render.ts +51 -39
- package/extensions/context-pruning/settings.ts +4 -5
- package/extensions/explore/README.md +13 -14
- package/extensions/explore/ast/adapter.ts +97 -0
- package/extensions/explore/ast/engine.ts +470 -0
- package/extensions/explore/ast/format/ast-search.ts +97 -0
- package/extensions/explore/ast/format/composite.ts +26 -0
- package/extensions/explore/ast/format/context.ts +66 -0
- package/extensions/explore/ast/format/deps.ts +86 -0
- package/extensions/explore/ast/format/discover.ts +85 -0
- package/extensions/explore/ast/format/impact.ts +105 -0
- package/extensions/explore/ast/format/outline.ts +66 -0
- package/extensions/explore/ast/format/relationships.ts +81 -0
- package/extensions/explore/ast/format/show.ts +38 -0
- package/extensions/explore/ast/grammars/kotlin.wasm +0 -0
- package/extensions/explore/ast/grammars/manifest.json +41 -0
- package/extensions/explore/ast/grammars/manifest.ts +99 -0
- package/extensions/explore/ast/grammars/odin.wasm +0 -0
- package/extensions/explore/ast/grammars/swift.wasm +0 -0
- package/extensions/explore/ast/graph/file-graph.ts +360 -0
- package/extensions/explore/ast/graph/relationships.ts +597 -0
- package/extensions/explore/ast/identity.ts +119 -0
- package/extensions/explore/ast/ir.ts +130 -0
- package/extensions/explore/ast/languages/csharp-file-deps.ts +189 -0
- package/extensions/explore/ast/languages/csharp.ts +493 -0
- package/extensions/explore/ast/languages/file-dep-util.ts +120 -0
- package/extensions/explore/ast/languages/fixtures/sample.cs +17 -0
- package/extensions/explore/ast/languages/fixtures/sample.java +21 -0
- package/extensions/explore/ast/languages/fixtures/sample.kt +15 -0
- package/extensions/explore/ast/languages/fixtures/sample.odin +28 -0
- package/extensions/explore/ast/languages/fixtures/sample.rs +31 -0
- package/extensions/explore/ast/languages/fixtures/sample.swift +25 -0
- package/extensions/explore/ast/languages/go-file-deps.ts +139 -0
- package/extensions/explore/ast/languages/go.ts +545 -0
- package/extensions/explore/ast/languages/java.ts +412 -0
- package/extensions/explore/ast/languages/jvm-file-deps.ts +217 -0
- package/extensions/explore/ast/languages/kotlin.ts +611 -0
- package/extensions/explore/ast/languages/odin-file-deps.ts +175 -0
- package/extensions/explore/ast/languages/odin.ts +407 -0
- package/extensions/explore/ast/languages/rust-file-deps.ts +159 -0
- package/extensions/explore/ast/languages/rust.ts +506 -0
- package/extensions/explore/ast/languages/swift-file-deps.ts +59 -0
- package/extensions/explore/ast/languages/swift.ts +466 -0
- package/extensions/explore/ast/languages/tree.ts +220 -0
- package/extensions/explore/ast/languages/typescript-file-deps.ts +259 -0
- package/extensions/explore/ast/languages/typescript-package-surface.ts +440 -0
- package/extensions/explore/ast/languages/typescript.ts +716 -0
- package/extensions/explore/ast/markdown.ts +145 -0
- package/extensions/explore/ast/package-surface.ts +42 -0
- package/extensions/explore/ast/queries/ast-search.ts +306 -0
- package/extensions/explore/ast/queries/composite-target.ts +77 -0
- package/extensions/explore/ast/queries/context.ts +406 -0
- package/extensions/explore/ast/queries/discover.ts +427 -0
- package/extensions/explore/ast/queries/impact.ts +250 -0
- package/extensions/explore/ast/queries/outline.ts +203 -0
- package/extensions/explore/ast/queries/show.ts +262 -0
- package/extensions/explore/ast/query.ts +48 -0
- package/extensions/explore/ast/read/hook.ts +125 -0
- package/extensions/explore/ast/read/policy.ts +55 -0
- package/extensions/explore/ast/registry.ts +120 -0
- package/extensions/explore/ast/scan.ts +76 -0
- package/extensions/explore/ast/slice.ts +31 -0
- package/extensions/explore/ast/tools/ast-search.ts +138 -0
- package/extensions/explore/ast/tools/context.ts +126 -0
- package/extensions/explore/ast/tools/deps.ts +72 -0
- package/extensions/explore/ast/tools/discover.ts +208 -0
- package/extensions/explore/ast/tools/impact.ts +135 -0
- package/extensions/explore/ast/tools/outline.ts +200 -0
- package/extensions/explore/ast/tools/relationships.ts +160 -0
- package/extensions/explore/ast/tools/render.ts +121 -0
- package/extensions/explore/ast/tools/reverse-deps.ts +72 -0
- package/extensions/explore/ast/tools/show.ts +116 -0
- package/extensions/explore/guidance.ts +52 -0
- package/extensions/explore/index.ts +91 -45
- package/extensions/explore/read/autoread.ts +118 -0
- package/extensions/explore/settings.ts +64 -0
- package/extensions/explore/traverse.ts +260 -86
- package/extensions/handoff/index.ts +1 -1
- package/extensions/patch/executor.ts +95 -1
- package/extensions/patch/index.ts +1 -0
- package/extensions/publish/index.ts +18 -85
- package/extensions/publish/publish-ui.ts +0 -4
- package/extensions/soul/README.md +7 -4
- package/extensions/soul/index.ts +11 -5
- package/extensions/soul/prompt.ts +30 -14
- package/extensions/soul/settings.ts +7 -2
- package/extensions/subagent/agents/context-sync.md +6 -10
- package/extensions/subagent/agents/review.md +44 -24
- package/extensions/subagent/agents/scout.md +53 -34
- package/extensions/subagent/run.ts +1 -1
- package/extensions/tau-help/help.md +2 -2
- package/package.json +6 -5
- package/schemas/tau.schema.json +51 -6
- package/{extensions/explore → shared}/autoread.ts +21 -7
- package/shared/bounded-text-result.ts +304 -0
- package/shared/events.ts +1 -0
- package/shared/full-file-knowledge.ts +49 -0
- package/shared/temporary-output-store.ts +275 -0
- package/extensions/explore/ast-tools.ts +0 -828
- package/extensions/explore/ast-worker.ts +0 -356
- package/extensions/explore/find.ts +0 -147
- package/extensions/explore/full-file-knowledge.ts +0 -234
- package/extensions/explore/grep.ts +0 -749
- package/extensions/explore/limits.ts +0 -22
- package/extensions/explore/ls.ts +0 -113
- package/extensions/explore/path-display.ts +0 -41
- package/extensions/explore/path-tree.ts +0 -150
- package/extensions/explore/read-cache.ts +0 -273
- package/extensions/explore/read-snapshots.ts +0 -57
- package/extensions/explore/read-stats-panel.ts +0 -154
- package/extensions/explore/read-stats.ts +0 -120
- package/extensions/explore/read.ts +0 -335
- package/extensions/explore/result.ts +0 -29
- package/native-bin/darwin-arm64/tau-ast +0 -0
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import { prepareAutoreadMessage, registerAutoread, type PreparedAutoreadMessage } from "../../../shared/autoread.ts";
|
|
5
|
+
import { createCompleteFileMeta } from "../../../shared/full-file-knowledge.ts";
|
|
6
|
+
import { loadTauExtensionSettings } from "../../../shared/settings/load.ts";
|
|
7
|
+
import type { ToolRowStateStore } from "../../../shared/tool-row-state.ts";
|
|
8
|
+
import type { ExploreEngine } from "../ast/engine.ts";
|
|
9
|
+
import { formatOutlineFile } from "../ast/format/outline.ts";
|
|
10
|
+
import { formatLargeReadOutline, shouldOutlineFullRead, type ExploreReadSettings } from "../ast/read/policy.ts";
|
|
11
|
+
import { outlinePath } from "../ast/queries/outline.ts";
|
|
12
|
+
import exploreSettings from "../settings.ts";
|
|
13
|
+
import { resolveExplorePath } from "../traverse.ts";
|
|
14
|
+
|
|
15
|
+
const DEFAULT_OUTLINE_OPTIONS = {
|
|
16
|
+
includePrivate: false,
|
|
17
|
+
includeDocs: false,
|
|
18
|
+
names: [] as readonly string[],
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export function registerExploreAutoread(
|
|
22
|
+
pi: ExtensionAPI,
|
|
23
|
+
rowState: ToolRowStateStore,
|
|
24
|
+
engineFor: (cwd: string) => ExploreEngine,
|
|
25
|
+
): void {
|
|
26
|
+
let readSettings: ExploreReadSettings = exploreSettings.defaults.read;
|
|
27
|
+
|
|
28
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
29
|
+
const settings = await loadTauExtensionSettings(ctx, exploreSettings);
|
|
30
|
+
readSettings = settings.read;
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
registerAutoread(pi, rowState, async (options) => {
|
|
34
|
+
return prepareExploreAutoreadMessage({
|
|
35
|
+
...options,
|
|
36
|
+
engine: engineFor(options.cwd),
|
|
37
|
+
readSettings,
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function prepareExploreAutoreadMessage(options: {
|
|
43
|
+
rowId: string;
|
|
44
|
+
path: string;
|
|
45
|
+
cwd: string;
|
|
46
|
+
source: string;
|
|
47
|
+
batchId: string;
|
|
48
|
+
signal: AbortSignal | undefined;
|
|
49
|
+
isLifecycleCurrent: () => boolean;
|
|
50
|
+
maximumBytes?: number;
|
|
51
|
+
engine: ExploreEngine;
|
|
52
|
+
readSettings: ExploreReadSettings;
|
|
53
|
+
}): Promise<PreparedAutoreadMessage> {
|
|
54
|
+
const { engine, readSettings, ...shared } = options;
|
|
55
|
+
if (!readSettings.enabled) return prepareAutoreadMessage(shared);
|
|
56
|
+
|
|
57
|
+
const absolutePath = resolveExplorePath(options.cwd, options.path);
|
|
58
|
+
if (engine.registry.adapterForPath(absolutePath) === undefined) {
|
|
59
|
+
return prepareAutoreadMessage(shared);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
assertPreparationCurrent(options.signal, options.isLifecycleCurrent);
|
|
63
|
+
|
|
64
|
+
let lineCount: number;
|
|
65
|
+
try {
|
|
66
|
+
const source = await engine.sourceForFile(absolutePath);
|
|
67
|
+
lineCount = source.ir.lineCount;
|
|
68
|
+
} catch {
|
|
69
|
+
return prepareAutoreadMessage(shared);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (!shouldOutlineFullRead(lineCount, readSettings.structureThresholdLines)) {
|
|
73
|
+
return prepareAutoreadMessage(shared);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const abort = options.signal ?? new AbortController().signal;
|
|
77
|
+
const result = await outlinePath(engine, options.path, DEFAULT_OUTLINE_OPTIONS, abort);
|
|
78
|
+
assertPreparationCurrent(options.signal, options.isLifecycleCurrent);
|
|
79
|
+
if (result.mode !== "file") {
|
|
80
|
+
return prepareAutoreadMessage(shared);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const outlineBody =
|
|
84
|
+
result.file.rows.length === 0 ? "No declarations" : formatOutlineFile(result.file, engine.cwd, false);
|
|
85
|
+
const outlineText = formatLargeReadOutline(outlineBody);
|
|
86
|
+
const messageContent = `${options.path}\n${outlineText}`;
|
|
87
|
+
const pathKey = resolve(options.cwd, options.path);
|
|
88
|
+
const readCache = createCompleteFileMeta({
|
|
89
|
+
pathKey,
|
|
90
|
+
presentation: "plain",
|
|
91
|
+
servedHash: createHash("sha256").update(outlineText, "utf8").digest("hex"),
|
|
92
|
+
mode: "baseline",
|
|
93
|
+
sourceText: outlineText,
|
|
94
|
+
returnedText: messageContent,
|
|
95
|
+
totalLines: lineCount,
|
|
96
|
+
summary: `outline (${lineCount} lines)`,
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
customType: "tau.autoread",
|
|
101
|
+
content: messageContent,
|
|
102
|
+
display: true,
|
|
103
|
+
details: {
|
|
104
|
+
rowId: options.rowId,
|
|
105
|
+
path: options.path,
|
|
106
|
+
cwd: options.cwd,
|
|
107
|
+
source: options.source,
|
|
108
|
+
batchId: options.batchId,
|
|
109
|
+
status: "read",
|
|
110
|
+
readCache,
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function assertPreparationCurrent(signal: AbortSignal | undefined, isLifecycleCurrent: () => boolean): void {
|
|
116
|
+
signal?.throwIfAborted();
|
|
117
|
+
if (!isLifecycleCurrent()) throw new Error("Autoread preparation crossed a session lifecycle boundary");
|
|
118
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { Type } from "typebox";
|
|
2
|
+
import { defineTauExtensionSettings } from "../../shared/settings/define.ts";
|
|
3
|
+
|
|
4
|
+
export default defineTauExtensionSettings({
|
|
5
|
+
key: "explore",
|
|
6
|
+
defaults: {
|
|
7
|
+
context: {
|
|
8
|
+
defaultBudgetTokens: 8000 as number,
|
|
9
|
+
},
|
|
10
|
+
read: {
|
|
11
|
+
enabled: true as boolean,
|
|
12
|
+
structureThresholdLines: 200 as number,
|
|
13
|
+
maxRangeLines: 200 as number,
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
schema: Type.Object(
|
|
17
|
+
{
|
|
18
|
+
context: Type.Optional(
|
|
19
|
+
Type.Object(
|
|
20
|
+
{
|
|
21
|
+
defaultBudgetTokens: Type.Optional(
|
|
22
|
+
Type.Integer({
|
|
23
|
+
default: 8000,
|
|
24
|
+
minimum: 1,
|
|
25
|
+
description: "Default token budget for context when the caller omits budget.",
|
|
26
|
+
}),
|
|
27
|
+
),
|
|
28
|
+
},
|
|
29
|
+
{ additionalProperties: false },
|
|
30
|
+
),
|
|
31
|
+
),
|
|
32
|
+
read: Type.Optional(
|
|
33
|
+
Type.Object(
|
|
34
|
+
{
|
|
35
|
+
enabled: Type.Optional(
|
|
36
|
+
Type.Boolean({
|
|
37
|
+
default: true,
|
|
38
|
+
description:
|
|
39
|
+
"Master switch for Explore structural read overlay and large-source autoread outline path. When false, Pi read and autoread keep ordinary full bodies.",
|
|
40
|
+
}),
|
|
41
|
+
),
|
|
42
|
+
structureThresholdLines: Type.Optional(
|
|
43
|
+
Type.Integer({
|
|
44
|
+
default: 200,
|
|
45
|
+
minimum: 1,
|
|
46
|
+
description:
|
|
47
|
+
"Registered source at or under this line count may be full-read. Above it, full read model-visible result is outline only.",
|
|
48
|
+
}),
|
|
49
|
+
),
|
|
50
|
+
maxRangeLines: Type.Optional(
|
|
51
|
+
Type.Integer({
|
|
52
|
+
default: 200,
|
|
53
|
+
minimum: 1,
|
|
54
|
+
description: "Maximum lines returned by one ranged read on registered source.",
|
|
55
|
+
}),
|
|
56
|
+
),
|
|
57
|
+
},
|
|
58
|
+
{ additionalProperties: false },
|
|
59
|
+
),
|
|
60
|
+
),
|
|
61
|
+
},
|
|
62
|
+
{ additionalProperties: false },
|
|
63
|
+
),
|
|
64
|
+
});
|
|
@@ -1,39 +1,80 @@
|
|
|
1
1
|
import type { Stats } from "node:fs";
|
|
2
|
-
import { lstat,
|
|
3
|
-
import {
|
|
4
|
-
|
|
2
|
+
import { lstat, opendir, readFile } from "node:fs/promises";
|
|
3
|
+
import { isAbsolute, join, matchesGlob, relative, resolve, sep } from "node:path";
|
|
4
|
+
|
|
5
|
+
/** Default recursive budgets for structural directory ops (path-conventions.md). */
|
|
6
|
+
export const DEFAULT_TRAVERSAL_BUDGETS = {
|
|
7
|
+
maxFiles: 2000,
|
|
8
|
+
maxSourceBytes: 64 * 1024 * 1024,
|
|
9
|
+
maxDepth: 32,
|
|
10
|
+
maxElapsedMs: 20_000,
|
|
11
|
+
} as const;
|
|
12
|
+
|
|
13
|
+
export type TraversalBudgets = {
|
|
14
|
+
maxFiles: number;
|
|
15
|
+
maxSourceBytes: number;
|
|
16
|
+
maxDepth: number;
|
|
17
|
+
maxElapsedMs: number;
|
|
18
|
+
};
|
|
5
19
|
|
|
6
20
|
export type PathKind = "file" | "dir";
|
|
7
21
|
|
|
8
|
-
export
|
|
22
|
+
export type TraversalHit = {
|
|
9
23
|
absolutePath: string;
|
|
10
24
|
displayPath: string;
|
|
11
25
|
type: PathKind;
|
|
12
26
|
stats: Stats;
|
|
13
27
|
depth: number;
|
|
14
|
-
|
|
15
|
-
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export type TraversalLimit = "maxFiles" | "maxSourceBytes" | "maxDepth" | "maxElapsedMs" | "cancelled";
|
|
16
31
|
|
|
17
|
-
export
|
|
32
|
+
export type TraversalResult = {
|
|
33
|
+
entries: TraversalHit[];
|
|
34
|
+
/** Set when a budget or cancellation stopped the walk before the tree was complete. */
|
|
35
|
+
limit: TraversalLimit | undefined;
|
|
36
|
+
filesVisited: number;
|
|
37
|
+
sourceBytes: number;
|
|
38
|
+
elapsedMs: number;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export type WalkOptions = {
|
|
18
42
|
cwd: string;
|
|
19
43
|
root: string;
|
|
20
|
-
|
|
21
|
-
includeRoot
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
44
|
+
/** Include the root path itself when it is a file or directory. Default true. */
|
|
45
|
+
includeRoot?: boolean;
|
|
46
|
+
/** Descend into hidden path segments (name starts with `.`). Default false. */
|
|
47
|
+
includeHidden?: boolean;
|
|
48
|
+
/** Include paths matching gitignore-style rules. Default false. */
|
|
49
|
+
includeIgnored?: boolean;
|
|
50
|
+
/** Include common noise directories (node_modules, dist, …). Default false. */
|
|
51
|
+
includeNoise?: boolean;
|
|
52
|
+
/** Only yield files (skip directory entries in the result list). Default true. */
|
|
53
|
+
filesOnly?: boolean;
|
|
54
|
+
budgets?: Partial<TraversalBudgets>;
|
|
55
|
+
signal?: AbortSignal;
|
|
56
|
+
/**
|
|
57
|
+
* Called after each accepted file. Return false to stop early without a budget limit
|
|
58
|
+
* (caller owns the stop reason). Async allowed for cooperative yielding.
|
|
59
|
+
*/
|
|
60
|
+
onFile?: (hit: TraversalHit) => void | boolean | Promise<void | boolean>;
|
|
61
|
+
/**
|
|
62
|
+
* If set, files for which this returns false are skipped without counting toward budgets.
|
|
63
|
+
* Directories are not filtered here.
|
|
64
|
+
*/
|
|
65
|
+
matchFile?: (hit: TraversalHit) => boolean;
|
|
66
|
+
};
|
|
26
67
|
|
|
27
|
-
|
|
68
|
+
type IgnoreRule = {
|
|
28
69
|
baseDir: string;
|
|
29
70
|
pattern: string;
|
|
30
71
|
negated: boolean;
|
|
31
72
|
dirOnly: boolean;
|
|
32
73
|
hasSlash: boolean;
|
|
33
74
|
anchored: boolean;
|
|
34
|
-
}
|
|
75
|
+
};
|
|
35
76
|
|
|
36
|
-
const
|
|
77
|
+
const NOISE_NAMES = new Set([
|
|
37
78
|
".git",
|
|
38
79
|
"node_modules",
|
|
39
80
|
"dist",
|
|
@@ -46,20 +87,65 @@ const NOISE_PATH_NAMES = new Set([
|
|
|
46
87
|
"out",
|
|
47
88
|
]);
|
|
48
89
|
|
|
49
|
-
function
|
|
50
|
-
return
|
|
90
|
+
export function stripLeadingAt(value: string): string {
|
|
91
|
+
return value.startsWith("@") ? value.slice(1) : value;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function toSlashPath(value: string): string {
|
|
95
|
+
return value.replaceAll("\\", "/");
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function resolveExplorePath(cwd: string, input: string): string {
|
|
99
|
+
const path = stripLeadingAt(input);
|
|
100
|
+
if (path.length === 0) return resolve(cwd);
|
|
101
|
+
return isAbsolute(path) ? resolve(path) : resolve(cwd, path);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function pathResolutionError(error: unknown, input: string): Error {
|
|
105
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
106
|
+
return new Error(`Path not found: ${stripLeadingAt(input)}`);
|
|
107
|
+
}
|
|
108
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function isWithinPath(parent: string, child: string): boolean {
|
|
112
|
+
const rel = relative(resolve(parent), resolve(child));
|
|
113
|
+
return rel === "" || (!rel.startsWith("..") && !isAbsolute(rel));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function formatPathForDisplay(absolutePath: string, cwd: string): string {
|
|
117
|
+
const resolvedCwd = resolve(cwd);
|
|
118
|
+
const resolvedPath = resolve(absolutePath);
|
|
119
|
+
const rel = relative(resolvedCwd, resolvedPath);
|
|
120
|
+
if (rel === "") return ".";
|
|
121
|
+
if (!rel.startsWith("..") && !isAbsolute(rel)) return toSlashPath(rel);
|
|
122
|
+
return toSlashPath(resolvedPath);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function relativeSlash(from: string, to: string): string {
|
|
126
|
+
const rel = relative(resolve(from), resolve(to));
|
|
127
|
+
return rel === "" ? "." : toSlashPath(rel);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function mergeBudgets(partial: Partial<TraversalBudgets> | undefined): TraversalBudgets {
|
|
131
|
+
return {
|
|
132
|
+
maxFiles: partial?.maxFiles ?? DEFAULT_TRAVERSAL_BUDGETS.maxFiles,
|
|
133
|
+
maxSourceBytes: partial?.maxSourceBytes ?? DEFAULT_TRAVERSAL_BUDGETS.maxSourceBytes,
|
|
134
|
+
maxDepth: partial?.maxDepth ?? DEFAULT_TRAVERSAL_BUDGETS.maxDepth,
|
|
135
|
+
maxElapsedMs: partial?.maxElapsedMs ?? DEFAULT_TRAVERSAL_BUDGETS.maxElapsedMs,
|
|
136
|
+
};
|
|
51
137
|
}
|
|
52
138
|
|
|
53
139
|
function pathSegments(displayPath: string): string[] {
|
|
54
140
|
return displayPath.split("/").filter((segment) => segment.length > 0 && segment !== ".");
|
|
55
141
|
}
|
|
56
142
|
|
|
57
|
-
function
|
|
143
|
+
function hasHiddenSegment(displayPath: string): boolean {
|
|
58
144
|
return pathSegments(displayPath).some((segment) => segment.startsWith("."));
|
|
59
145
|
}
|
|
60
146
|
|
|
61
|
-
function
|
|
62
|
-
return pathSegments(displayPath).some((segment) =>
|
|
147
|
+
function hasNoiseSegment(displayPath: string): boolean {
|
|
148
|
+
return pathSegments(displayPath).some((segment) => NOISE_NAMES.has(segment));
|
|
63
149
|
}
|
|
64
150
|
|
|
65
151
|
function parseIgnoreLine(baseDir: string, rawLine: string): IgnoreRule | undefined {
|
|
@@ -74,7 +160,14 @@ function parseIgnoreLine(baseDir: string, rawLine: string): IgnoreRule | undefin
|
|
|
74
160
|
if (anchored) line = line.slice(1);
|
|
75
161
|
line = toSlashPath(line);
|
|
76
162
|
if (!line) return undefined;
|
|
77
|
-
return {
|
|
163
|
+
return {
|
|
164
|
+
baseDir,
|
|
165
|
+
pattern: line,
|
|
166
|
+
negated,
|
|
167
|
+
dirOnly,
|
|
168
|
+
hasSlash: line.includes("/"),
|
|
169
|
+
anchored,
|
|
170
|
+
};
|
|
78
171
|
}
|
|
79
172
|
|
|
80
173
|
async function readIgnoreRules(baseDir: string): Promise<IgnoreRule[]> {
|
|
@@ -89,27 +182,27 @@ async function readIgnoreRules(baseDir: string): Promise<IgnoreRule[]> {
|
|
|
89
182
|
}
|
|
90
183
|
}
|
|
91
184
|
|
|
92
|
-
async function
|
|
93
|
-
directory
|
|
94
|
-
inherited:
|
|
95
|
-
): Promise<IgnoreRule[]> {
|
|
96
|
-
const rules = await readIgnoreRules(directory);
|
|
97
|
-
return rules.length === 0 ? [...inherited] : [...inherited, ...rules];
|
|
185
|
+
async function appendIgnoreRules(directory: string, inherited: readonly IgnoreRule[]): Promise<IgnoreRule[]> {
|
|
186
|
+
const local = await readIgnoreRules(directory);
|
|
187
|
+
return local.length === 0 ? [...inherited] : [...inherited, ...local];
|
|
98
188
|
}
|
|
99
189
|
|
|
100
190
|
async function collectIgnoreRulesToDirectory(cwd: string, directory: string): Promise<IgnoreRule[]> {
|
|
101
191
|
const resolvedCwd = resolve(cwd);
|
|
102
192
|
const resolvedDirectory = resolve(directory);
|
|
103
|
-
|
|
193
|
+
// Outside the session cwd (absolute foreign roots): load that directory's own ignore only.
|
|
194
|
+
if (!isWithinPath(resolvedCwd, resolvedDirectory)) {
|
|
195
|
+
return appendIgnoreRules(resolvedDirectory, []);
|
|
196
|
+
}
|
|
104
197
|
|
|
105
198
|
let current = resolvedCwd;
|
|
106
|
-
let rules
|
|
199
|
+
let rules = await appendIgnoreRules(current, []);
|
|
107
200
|
const rel = relativeSlash(resolvedCwd, resolvedDirectory);
|
|
108
201
|
if (rel === ".") return rules;
|
|
109
202
|
|
|
110
203
|
for (const segment of rel.split("/")) {
|
|
111
204
|
current = join(current, segment.split("/").join(sep));
|
|
112
|
-
rules = await
|
|
205
|
+
rules = await appendIgnoreRules(current, rules);
|
|
113
206
|
}
|
|
114
207
|
return rules;
|
|
115
208
|
}
|
|
@@ -130,68 +223,153 @@ function isIgnored(absolutePath: string, kind: PathKind, rules: readonly IgnoreR
|
|
|
130
223
|
return ignored;
|
|
131
224
|
}
|
|
132
225
|
|
|
133
|
-
function
|
|
226
|
+
function shouldSkip(
|
|
134
227
|
absolutePath: string,
|
|
135
228
|
kind: PathKind,
|
|
136
229
|
rules: readonly IgnoreRule[],
|
|
137
|
-
options:
|
|
230
|
+
options: {
|
|
231
|
+
walkRoot: string;
|
|
232
|
+
includeHidden: boolean;
|
|
233
|
+
includeIgnored: boolean;
|
|
234
|
+
includeNoise: boolean;
|
|
235
|
+
},
|
|
138
236
|
): boolean {
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
237
|
+
// Hidden/noise are relative to the walk root — not the session cwd display path.
|
|
238
|
+
// Otherwise absolute roots under ~/.local (etc.) look entirely "hidden".
|
|
239
|
+
const rel = relativeSlash(options.walkRoot, absolutePath);
|
|
240
|
+
const underRoot = rel === "." || (!rel.startsWith("..") && !isAbsolute(rel));
|
|
241
|
+
const segmentPath = underRoot && rel !== "." ? rel : "";
|
|
242
|
+
if (!options.includeHidden && segmentPath.length > 0 && hasHiddenSegment(segmentPath)) return true;
|
|
243
|
+
if (!options.includeNoise && segmentPath.length > 0 && hasNoiseSegment(segmentPath)) return true;
|
|
142
244
|
return !options.includeIgnored && isIgnored(absolutePath, kind, rules);
|
|
143
245
|
}
|
|
144
246
|
|
|
145
|
-
function
|
|
146
|
-
return
|
|
247
|
+
function entryKind(stats: Stats): PathKind {
|
|
248
|
+
return stats.isDirectory() ? "dir" : "file";
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function sortHits(hits: TraversalHit[]): TraversalHit[] {
|
|
252
|
+
return hits.sort((a, b) => {
|
|
147
253
|
if (a.type !== b.type) return a.type === "dir" ? -1 : 1;
|
|
148
254
|
return a.displayPath.localeCompare(b.displayPath);
|
|
149
255
|
});
|
|
150
256
|
}
|
|
151
257
|
|
|
152
|
-
async function
|
|
258
|
+
async function listNames(directory: string, cwd: string): Promise<string[]> {
|
|
153
259
|
try {
|
|
154
|
-
|
|
260
|
+
const names: string[] = [];
|
|
261
|
+
for await (const entry of await opendir(directory)) {
|
|
262
|
+
names.push(entry.name);
|
|
263
|
+
}
|
|
264
|
+
return names;
|
|
155
265
|
} catch {
|
|
156
266
|
throw new Error(`Cannot read directory: ${formatPathForDisplay(directory, cwd)}`);
|
|
157
267
|
}
|
|
158
268
|
}
|
|
159
269
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
const
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
270
|
+
/**
|
|
271
|
+
* Ignore-aware directory walk for structural scans.
|
|
272
|
+
* Budgets (files / source bytes / depth / elapsed) are separate from model output limits.
|
|
273
|
+
* Hitting a budget sets `result.limit` — not silent complete-tree success.
|
|
274
|
+
*/
|
|
275
|
+
export async function walkPaths(options: WalkOptions): Promise<TraversalResult> {
|
|
276
|
+
const budgets = mergeBudgets(options.budgets);
|
|
277
|
+
const includeRoot = options.includeRoot !== false;
|
|
278
|
+
const includeHidden = options.includeHidden === true;
|
|
279
|
+
const includeIgnored = options.includeIgnored === true;
|
|
280
|
+
const includeNoise = options.includeNoise === true;
|
|
281
|
+
const filesOnly = options.filesOnly !== false;
|
|
282
|
+
const started = Date.now();
|
|
283
|
+
|
|
284
|
+
const entries: TraversalHit[] = [];
|
|
285
|
+
let filesVisited = 0;
|
|
286
|
+
let sourceBytes = 0;
|
|
287
|
+
let limit: TraversalLimit | undefined;
|
|
288
|
+
|
|
289
|
+
const elapsed = () => Date.now() - started;
|
|
290
|
+
|
|
291
|
+
const budgetHit = (): TraversalLimit | undefined => {
|
|
292
|
+
if (options.signal?.aborted) return "cancelled";
|
|
293
|
+
if (elapsed() >= budgets.maxElapsedMs) return "maxElapsedMs";
|
|
294
|
+
if (filesVisited >= budgets.maxFiles) return "maxFiles";
|
|
295
|
+
if (sourceBytes > budgets.maxSourceBytes) return "maxSourceBytes";
|
|
296
|
+
return undefined;
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
const stopIfNeeded = (): boolean => {
|
|
300
|
+
const hit = budgetHit();
|
|
301
|
+
if (hit === undefined) return false;
|
|
302
|
+
limit = limit ?? hit;
|
|
303
|
+
return true;
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
async function pushFile(hit: TraversalHit): Promise<boolean> {
|
|
307
|
+
if (options.matchFile !== undefined && !options.matchFile(hit)) return true;
|
|
308
|
+
if (stopIfNeeded()) return false;
|
|
309
|
+
if (filesVisited >= budgets.maxFiles) {
|
|
310
|
+
limit = "maxFiles";
|
|
311
|
+
return false;
|
|
312
|
+
}
|
|
313
|
+
const nextBytes = sourceBytes + hit.stats.size;
|
|
314
|
+
// Allow the first file even if it alone exceeds the byte budget; report the limit.
|
|
315
|
+
if (filesVisited > 0 && nextBytes > budgets.maxSourceBytes) {
|
|
316
|
+
limit = "maxSourceBytes";
|
|
317
|
+
return false;
|
|
318
|
+
}
|
|
319
|
+
filesVisited += 1;
|
|
320
|
+
sourceBytes = nextBytes;
|
|
321
|
+
if (sourceBytes > budgets.maxSourceBytes) limit = "maxSourceBytes";
|
|
322
|
+
entries.push(hit);
|
|
323
|
+
const cont = await options.onFile?.(hit);
|
|
324
|
+
if (cont === false) return false;
|
|
325
|
+
return !stopIfNeeded();
|
|
179
326
|
}
|
|
180
327
|
|
|
181
|
-
|
|
182
|
-
|
|
328
|
+
const root = resolveExplorePath(options.cwd, options.root);
|
|
329
|
+
const rootStats = await lstat(root);
|
|
330
|
+
const rootKind = entryKind(rootStats);
|
|
331
|
+
const rootDir = rootKind === "dir" ? root : resolve(root, "..");
|
|
332
|
+
const walkRoot = rootKind === "dir" ? root : rootDir;
|
|
333
|
+
const skipOpts = { walkRoot, includeHidden, includeIgnored, includeNoise };
|
|
334
|
+
const rootRules = includeIgnored ? [] : await collectIgnoreRulesToDirectory(options.cwd, rootDir);
|
|
183
335
|
|
|
184
|
-
|
|
185
|
-
|
|
336
|
+
if (includeRoot) {
|
|
337
|
+
const rootHit: TraversalHit = {
|
|
338
|
+
absolutePath: root,
|
|
339
|
+
displayPath: formatPathForDisplay(root, options.cwd),
|
|
340
|
+
type: rootKind,
|
|
341
|
+
stats: rootStats,
|
|
342
|
+
depth: 0,
|
|
343
|
+
};
|
|
344
|
+
if (rootKind === "file") {
|
|
345
|
+
await pushFile(rootHit);
|
|
346
|
+
return { entries, limit, filesVisited, sourceBytes, elapsedMs: elapsed() };
|
|
347
|
+
}
|
|
348
|
+
if (!filesOnly) entries.push(rootHit);
|
|
349
|
+
} else if (rootKind === "file") {
|
|
350
|
+
return { entries, limit, filesVisited, sourceBytes, elapsedMs: elapsed() };
|
|
351
|
+
}
|
|
186
352
|
|
|
187
|
-
|
|
353
|
+
async function walkDir(directory: string, depth: number, rules: readonly IgnoreRule[]): Promise<void> {
|
|
354
|
+
if (stopIfNeeded()) return;
|
|
355
|
+
if (depth >= budgets.maxDepth) {
|
|
356
|
+
limit = limit ?? "maxDepth";
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
188
359
|
|
|
189
|
-
const
|
|
360
|
+
const names = await listNames(directory, options.cwd);
|
|
361
|
+
const children: TraversalHit[] = [];
|
|
190
362
|
for (const name of names) {
|
|
191
|
-
|
|
192
|
-
const
|
|
363
|
+
if (stopIfNeeded()) return;
|
|
364
|
+
const childPath = join(directory, name);
|
|
365
|
+
let stats: Stats;
|
|
366
|
+
try {
|
|
367
|
+
stats = await lstat(childPath);
|
|
368
|
+
} catch {
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
193
371
|
const kind = entryKind(stats);
|
|
194
|
-
if (
|
|
372
|
+
if (shouldSkip(childPath, kind, rules, skipOpts)) continue;
|
|
195
373
|
children.push({
|
|
196
374
|
absolutePath: childPath,
|
|
197
375
|
displayPath: formatPathForDisplay(childPath, options.cwd),
|
|
@@ -201,26 +379,22 @@ export async function collectPaths(options: CollectPathOptions): Promise<Travers
|
|
|
201
379
|
});
|
|
202
380
|
}
|
|
203
381
|
|
|
204
|
-
for (const child of
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
? rules
|
|
210
|
-
: await appendIgnoreRulesForDirectory(child.absolutePath, rules);
|
|
211
|
-
const visibleChildren = await walkDirectory(child.absolutePath, child.depth, childRules);
|
|
212
|
-
if (visibleChildren === 0 && (options.maxDepth === undefined || child.depth < options.maxDepth)) {
|
|
213
|
-
child.empty = true;
|
|
382
|
+
for (const child of sortHits(children)) {
|
|
383
|
+
if (stopIfNeeded()) return;
|
|
384
|
+
if (child.type === "file") {
|
|
385
|
+
if (!(await pushFile(child))) return;
|
|
386
|
+
continue;
|
|
214
387
|
}
|
|
388
|
+
if (!filesOnly) entries.push(child);
|
|
389
|
+
if (child.depth >= budgets.maxDepth) {
|
|
390
|
+
limit = limit ?? "maxDepth";
|
|
391
|
+
continue;
|
|
392
|
+
}
|
|
393
|
+
const childRules = includeIgnored ? rules : await appendIgnoreRules(child.absolutePath, rules);
|
|
394
|
+
await walkDir(child.absolutePath, child.depth, childRules);
|
|
215
395
|
}
|
|
216
|
-
|
|
217
|
-
return children.length;
|
|
218
396
|
}
|
|
219
397
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
if (rootEntry && visibleChildren === 0 && (options.maxDepth === undefined || options.maxDepth > 0)) {
|
|
223
|
-
rootEntry.empty = true;
|
|
224
|
-
}
|
|
225
|
-
return entries;
|
|
398
|
+
if (rootKind === "dir") await walkDir(root, 0, rootRules);
|
|
399
|
+
return { entries, limit, filesVisited, sourceBytes, elapsedMs: elapsed() };
|
|
226
400
|
}
|
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
type ExtensionAPI,
|
|
7
7
|
serializeConversation,
|
|
8
8
|
} from "@earendil-works/pi-coding-agent";
|
|
9
|
-
import { prepareAutoreadMessage, type AutoreadDetails } from "
|
|
9
|
+
import { prepareAutoreadMessage, type AutoreadDetails } from "../../shared/autoread.ts";
|
|
10
10
|
import { generateToolValidated, resolveCandidates } from "../../shared/model-fallback/index.ts";
|
|
11
11
|
import { errorText } from "../../shared/text.ts";
|
|
12
12
|
import { buildHandoffRequest, type HandoffDraft, handoffDraftFromToolInput, HANDOFF_TOOL } from "./model.ts";
|