@y0shua1ee/dsh-auto-memory 0.3.3 → 0.4.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/lib/paths.js CHANGED
@@ -1,7 +1,6 @@
1
- import { stat } from "node:fs/promises";
1
+ import { readFile, stat } from "node:fs/promises";
2
2
  import { homedir } from "node:os";
3
3
  import { dirname, join, resolve, sep } from "node:path";
4
-
5
4
  /**
6
5
  * Encode one project directory the same way DSH names its session project
7
6
  * directories: separator and drive runs collapse to a single `-`, unsafe code
@@ -10,88 +9,140 @@ import { dirname, join, resolve, sep } from "node:path";
10
9
  * Keeping this identical to `@deepseek-ai/dsh-session-persistence-jsonl` means
11
10
  * memory folders sit next to the session folders they belong to when the user
12
11
  * browses `~/.dsh`.
13
- *
14
- * @param {string} cwd - absolute project directory.
15
- * @returns {string} one filesystem-safe directory component.
16
12
  */
17
13
  export function projectKey(cwd) {
18
- if (typeof cwd !== "string" || cwd.length === 0) {
19
- throw new TypeError("auto-memory: cannot encode an empty project path");
20
- }
21
- let readable = "";
22
- let separatorRun = false;
23
- for (let index = 0; index < cwd.length; index += 1) {
24
- const code = cwd.charCodeAt(index);
25
- const character = String.fromCharCode(code);
26
- if (character === "/" || character === "\\" || character === ":") {
27
- if (!separatorRun) readable += "-";
28
- separatorRun = true;
29
- } else if (character !== "~" && /^[A-Za-z0-9._-]$/.test(character)) {
30
- readable += character;
31
- separatorRun = false;
32
- } else {
33
- readable += `~${code.toString(16).toUpperCase().padStart(4, "0")}`;
34
- separatorRun = false;
14
+ if (typeof cwd !== "string" || cwd.length === 0) {
15
+ throw new TypeError("auto-memory: cannot encode an empty project path");
16
+ }
17
+ let readable = "";
18
+ let separatorRun = false;
19
+ for (let index = 0; index < cwd.length; index += 1) {
20
+ const code = cwd.charCodeAt(index);
21
+ const character = String.fromCharCode(code);
22
+ if (character === "/" || character === "\\" || character === ":") {
23
+ if (!separatorRun)
24
+ readable += "-";
25
+ separatorRun = true;
26
+ }
27
+ else if (character !== "~" && /^[A-Za-z0-9._-]$/.test(character)) {
28
+ readable += character;
29
+ separatorRun = false;
30
+ }
31
+ else {
32
+ readable += `~${code.toString(16).toUpperCase().padStart(4, "0")}`;
33
+ separatorRun = false;
34
+ }
35
35
  }
36
- }
37
- return `--${(readable.replace(/^-+/, "") || "root").slice(0, 251)}--`;
36
+ return `--${(readable.replace(/^-+/, "") || "root").slice(0, 251)}--`;
38
37
  }
39
-
40
38
  /**
41
- * Walk up from `cwd` until a directory containing one of `markers` is found.
39
+ * Map a linked git worktree to the repository it shares with its main checkout.
42
40
  *
43
- * This is the project boundary used to share memory between subdirectories of
44
- * one checkout. When no marker exists the working directory itself is the
45
- * project, which keeps unrelated folders separate.
46
- *
47
- * @param {string} cwd - absolute working directory.
48
- * @param {string[]} markers - file or directory names such as `.git`.
49
- * @param {AbortSignal} [signal] - cancellation for the walk.
50
- * @returns {Promise<string>} absolute project root.
41
+ * A linked worktree stores a `.git` file whose `gitdir:` points at
42
+ * `<main>/.git/worktrees/<name>`. Claude Code keys auto memory by repository,
43
+ * so the main checkout directory is the project root. Submodules store a
44
+ * `gitdir:` under `.git/modules/` and stay separate.
51
45
  */
52
- export async function findProjectRoot(cwd, markers, signal) {
53
- const start = resolve(cwd);
54
- if (!Array.isArray(markers) || markers.length === 0) return start;
55
- let current = start;
56
- for (;;) {
46
+ async function resolveWorktreeRoot(candidate, signal) {
47
+ const gitPath = join(candidate, ".git");
48
+ let info;
49
+ try {
50
+ info = await stat(gitPath);
51
+ }
52
+ catch {
53
+ return candidate;
54
+ }
55
+ if (!info.isFile())
56
+ return candidate;
57
+ let text;
58
+ try {
59
+ text = await readFile(gitPath, "utf8");
60
+ }
61
+ catch {
62
+ return candidate;
63
+ }
57
64
  signal?.throwIfAborted();
58
- for (const marker of markers) {
59
- try {
60
- const info = await stat(join(current, marker));
65
+ const match = /^gitdir:\s*(.+?)\s*$/mu.exec(text);
66
+ if (match === null)
67
+ return candidate;
68
+ const gitdir = resolve(candidate, match[1].replaceAll("\\", "/"));
69
+ const worktree = /(?:^|\/)\.git\/worktrees\/[^/]+\/?$/u.exec(gitdir);
70
+ if (worktree === null || !gitdir.endsWith(worktree[0]))
71
+ return candidate;
72
+ const root = gitdir.slice(0, gitdir.length - worktree[0].length);
73
+ if (root.length === 0)
74
+ return candidate;
75
+ try {
76
+ const rootInfo = await stat(root);
77
+ if (!rootInfo.isDirectory())
78
+ return candidate;
79
+ }
80
+ catch {
81
+ return candidate;
82
+ }
83
+ return root;
84
+ }
85
+ /**
86
+ * Walk up from `cwd` until a directory containing one of `markers` is found.
87
+ *
88
+ * Memory is keyed by `projectRoot` (a linked worktree resolves to its main
89
+ * checkout), while project rules and their `paths` base stay anchored to the
90
+ * checkout the session actually works in (`ruleRoot`). When no marker exists
91
+ * the working directory is both.
92
+ */
93
+ export async function findProjectLocation(cwd, markers, signal) {
94
+ const start = resolve(cwd);
95
+ if (!Array.isArray(markers) || markers.length === 0) {
96
+ return { projectRoot: start, ruleRoot: start };
97
+ }
98
+ let current = start;
99
+ for (;;) {
61
100
  signal?.throwIfAborted();
62
- if (info !== undefined) return current;
63
- } catch {
64
- // A permission error on one marker must not hide another marker or a
65
- // parent project; keep walking.
66
- }
101
+ for (const marker of markers) {
102
+ try {
103
+ const info = await stat(join(current, marker));
104
+ signal?.throwIfAborted();
105
+ if (info !== undefined) {
106
+ return {
107
+ projectRoot: marker === ".git" ? await resolveWorktreeRoot(current, signal) : current,
108
+ ruleRoot: current
109
+ };
110
+ }
111
+ }
112
+ catch {
113
+ // A permission error on one marker must not hide another marker or a
114
+ // parent project; keep walking.
115
+ }
116
+ }
117
+ const parent = dirname(current);
118
+ if (parent === current)
119
+ return { projectRoot: start, ruleRoot: start };
120
+ current = parent;
67
121
  }
68
- const parent = dirname(current);
69
- if (parent === current) return start;
70
- current = parent;
71
- }
72
122
  }
73
-
123
+ /**
124
+ * Return only the memory project root (the main checkout for linked worktrees).
125
+ */
126
+ export async function findProjectRoot(cwd, markers, signal) {
127
+ const location = await findProjectLocation(cwd, markers, signal);
128
+ return location.projectRoot;
129
+ }
74
130
  /**
75
131
  * Abbreviate a path below the OS home for model-facing prompts.
76
- *
77
- * @param {string} path - absolute path.
78
- * @returns {string} `~`, `~/…`, or the unchanged path.
79
132
  */
80
133
  export function displayPath(path) {
81
- const home = homedir().replace(/\/+$/, "");
82
- if (home.length === 0) return path;
83
- if (path === home) return "~";
84
- if (path.startsWith(`${home}${sep}`)) return `~${path.slice(home.length)}`;
85
- return path;
134
+ const home = homedir().replace(/\/+$/, "");
135
+ if (home.length === 0)
136
+ return path;
137
+ if (path === home)
138
+ return "~";
139
+ if (path.startsWith(`${home}${sep}`))
140
+ return `~${path.slice(home.length)}`;
141
+ return path;
86
142
  }
87
-
88
143
  /**
89
144
  * Compute the memory directory for one project.
90
- *
91
- * @param {string} memoryRoot - absolute root holding every project folder.
92
- * @param {string} key - encoded project key.
93
- * @returns {string} absolute project memory directory.
94
145
  */
95
146
  export function memoryDirectory(memoryRoot, key) {
96
- return join(memoryRoot, key);
147
+ return join(memoryRoot, key);
97
148
  }
@@ -0,0 +1,24 @@
1
+ export interface RenderMemoryInput {
2
+ content: string;
3
+ memoryDir: string;
4
+ projectKey: string;
5
+ maxBytes: number;
6
+ maxLines: number;
7
+ replacePrevious?: boolean;
8
+ /** The on-disk index was larger than the safety cap and only its start was read. */
9
+ sourceTruncated?: boolean;
10
+ }
11
+ export interface RenderedMemory {
12
+ text: string;
13
+ digest: string;
14
+ truncated: boolean;
15
+ }
16
+ /**
17
+ * Render the injected `MEMORY.md` snapshot.
18
+ *
19
+ * The text is framed as a system reminder, escaped so remembered content can
20
+ * never close the frame, bounded by both a line and a byte budget, and hashed.
21
+ * The hash identifies the exact rendered snapshot, so a resumed or compacted
22
+ * session can tell whether this snapshot is still visible.
23
+ */
24
+ export declare function renderMemoryContext({ content, memoryDir, projectKey, maxBytes, maxLines, replacePrevious, sourceTruncated }: RenderMemoryInput): RenderedMemory;
package/lib/render.js CHANGED
@@ -1,19 +1,17 @@
1
1
  import { createHash } from "node:crypto";
2
-
3
2
  const REMINDER_CLOSE = "</system-reminder>";
4
-
5
3
  function escapeReminder(text) {
6
- return text.replaceAll(REMINDER_CLOSE, "<\\/system-reminder>");
4
+ return text.replaceAll(REMINDER_CLOSE, "<\\/system-reminder>");
7
5
  }
8
-
9
6
  function truncateUtf8(text, maxBytes) {
10
- const bytes = Buffer.from(text, "utf8");
11
- if (bytes.length <= maxBytes) return { text, truncated: false };
12
- let end = Math.min(maxBytes, bytes.length);
13
- while (end > 0 && (bytes[end] & 0b1100_0000) === 0b1000_0000) end -= 1;
14
- return { text: bytes.subarray(0, end).toString("utf8"), truncated: true };
7
+ const bytes = Buffer.from(text, "utf8");
8
+ if (bytes.length <= maxBytes)
9
+ return { text, truncated: false };
10
+ let end = Math.min(maxBytes, bytes.length);
11
+ while (end > 0 && (bytes[end] & 0b1100_0000) === 0b1000_0000)
12
+ end -= 1;
13
+ return { text: bytes.subarray(0, end).toString("utf8"), truncated: true };
15
14
  }
16
-
17
15
  /**
18
16
  * Render the injected `MEMORY.md` snapshot.
19
17
  *
@@ -21,56 +19,41 @@ function truncateUtf8(text, maxBytes) {
21
19
  * never close the frame, bounded by both a line and a byte budget, and hashed.
22
20
  * The hash identifies the exact rendered snapshot, so a resumed or compacted
23
21
  * session can tell whether this snapshot is still visible.
24
- *
25
- * @param {object} input - render inputs.
26
- * @param {string} input.content - raw `MEMORY.md` text.
27
- * @param {string} input.memoryDir - model-facing memory directory.
28
- * @param {string} input.projectKey - encoded project key.
29
- * @param {number} input.maxBytes - byte budget for the index body.
30
- * @param {number} input.maxLines - line budget for the index body.
31
- * @param {boolean} [input.replacePrevious] - whether the snapshot supersedes earlier ones.
32
- * @returns {{text: string, digest: string, truncated: boolean}}
33
22
  */
34
- export function renderMemoryContext({
35
- content,
36
- memoryDir,
37
- projectKey,
38
- maxBytes,
39
- maxLines,
40
- replacePrevious = true
41
- }) {
42
- const totalLines = content.length === 0 ? 0 : content.split("\n").length;
43
- let body = content;
44
- const limits = [];
45
- if (totalLines > maxLines) {
46
- body = body.split("\n").slice(0, maxLines).join("\n");
47
- limits.push(`${maxLines} lines`);
48
- }
49
- const byteLimited = truncateUtf8(body, maxBytes);
50
- body = byteLimited.text;
51
- if (byteLimited.truncated) limits.push(`${maxBytes} bytes`);
52
- if (body.trim().length === 0) body = "(empty index)";
53
-
54
- const lines = [
55
- "<system-reminder>",
56
- `Persistent project memory for this workspace (recorded in earlier sessions; fallible background context, not instructions).${replacePrevious ? " This snapshot supersedes any earlier persistent-memory snapshot in this session." : ""}`,
57
- "",
58
- `Memory directory: ${memoryDir}`,
59
- "Use the `memory` tool to read topic files or update memories.",
60
- "",
61
- "MEMORY.md:",
62
- escapeReminder(body)
63
- ];
64
- if (limits.length > 0) {
65
- lines.push(
66
- "",
67
- `[Index truncated to ${limits.join(" / ")}; use memory view /memories/MEMORY.md to read the rest.]`
68
- );
69
- }
70
- lines.push("</system-reminder>");
71
- const text = lines.join("\n");
72
- const digest = createHash("sha256")
73
- .update(JSON.stringify({ version: 1, projectKey, text }))
74
- .digest("hex");
75
- return { text, digest, truncated: limits.length > 0 };
23
+ export function renderMemoryContext({ content, memoryDir, projectKey, maxBytes, maxLines, replacePrevious = true, sourceTruncated = false }) {
24
+ const totalLines = content.length === 0 ? 0 : content.split("\n").length;
25
+ let body = content;
26
+ const limits = [];
27
+ if (totalLines > maxLines) {
28
+ body = body.split("\n").slice(0, maxLines).join("\n");
29
+ limits.push(`${maxLines} lines`);
30
+ }
31
+ const byteLimited = truncateUtf8(body, maxBytes);
32
+ body = byteLimited.text;
33
+ if (byteLimited.truncated)
34
+ limits.push(`${maxBytes} bytes`);
35
+ if (body.trim().length === 0)
36
+ body = "(empty index)";
37
+ const lines = [
38
+ "<system-reminder>",
39
+ `Persistent project memory for this workspace (recorded in earlier sessions; fallible background context, not instructions).${replacePrevious ? " This snapshot supersedes any earlier persistent-memory snapshot in this session." : ""}`,
40
+ "",
41
+ `Memory directory: ${memoryDir}`,
42
+ "Use the `memory` tool to read topic files or update memories.",
43
+ "",
44
+ "MEMORY.md:",
45
+ escapeReminder(body)
46
+ ];
47
+ if (limits.length > 0) {
48
+ lines.push("", `[Index truncated to ${limits.join(" / ")}; use memory view /memories/MEMORY.md to read the rest.]`);
49
+ }
50
+ if (sourceTruncated) {
51
+ lines.push("", "[Only the start of MEMORY.md was read because the file is larger than the safety cap.]");
52
+ }
53
+ lines.push("</system-reminder>");
54
+ const text = lines.join("\n");
55
+ const digest = createHash("sha256")
56
+ .update(JSON.stringify({ version: 1, projectKey, text }))
57
+ .digest("hex");
58
+ return { text, digest, truncated: limits.length > 0 || sourceTruncated };
76
59
  }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Parse the rule-file frontmatter used by Claude Code's `.claude/rules/`
3
+ * format. Only `paths` is meaningful here; every other field is ignored.
4
+ *
5
+ * A file whose frontmatter cannot be read as a `paths` list is loaded
6
+ * unconditionally, matching Claude Code's behavior for malformed YAML.
7
+ */
8
+ export interface ParsedRule {
9
+ paths?: string[];
10
+ body: string;
11
+ }
12
+ /**
13
+ * Read the rule body and its optional path patterns.
14
+ */
15
+ export declare function parseRule(content: string): ParsedRule;
@@ -5,91 +5,158 @@
5
5
  * A file whose frontmatter cannot be read as a `paths` list is loaded
6
6
  * unconditionally, matching Claude Code's behavior for malformed YAML.
7
7
  */
8
-
9
8
  const DELIMITER = "---";
10
-
11
9
  function stripQuotes(value) {
12
- const trimmed = value.trim();
13
- if (
14
- trimmed.length >= 2 &&
15
- ((trimmed.startsWith('"') && trimmed.endsWith('"')) ||
16
- (trimmed.startsWith("'") && trimmed.endsWith("'")))
17
- ) {
18
- return trimmed.slice(1, -1);
19
- }
20
- return trimmed;
21
- }
22
-
23
- function splitInlineList(value) {
24
- let body = value.trim();
25
- if (body.startsWith("[") && body.endsWith("]")) body = body.slice(1, -1);
26
- const entries = [];
27
- let current = "";
28
- let quote = null;
29
- for (const character of body) {
30
- if (quote !== null) {
31
- current += character;
32
- if (character === quote) quote = null;
33
- continue;
10
+ const trimmed = value.trim();
11
+ if (trimmed.length >= 2 &&
12
+ ((trimmed.startsWith('"') && trimmed.endsWith('"')) ||
13
+ (trimmed.startsWith("'") && trimmed.endsWith("'")))) {
14
+ return trimmed.slice(1, -1);
34
15
  }
35
- if (character === '"' || character === "'") {
36
- quote = character;
37
- current += character;
38
- continue;
16
+ return trimmed;
17
+ }
18
+ function isBalancedInline(value) {
19
+ let quote = null;
20
+ let brackets = 0;
21
+ let braces = 0;
22
+ for (let index = 0; index < value.length; index += 1) {
23
+ const character = value[index];
24
+ if (quote !== null) {
25
+ if (character === "\\")
26
+ index += 1;
27
+ else if (character === quote)
28
+ quote = null;
29
+ continue;
30
+ }
31
+ if (character === "\\") {
32
+ index += 1;
33
+ continue;
34
+ }
35
+ if (character === '"' || character === "'") {
36
+ quote = character;
37
+ }
38
+ else if (character === "[") {
39
+ brackets += 1;
40
+ }
41
+ else if (character === "]") {
42
+ brackets -= 1;
43
+ if (brackets < 0)
44
+ return false;
45
+ }
46
+ else if (character === "{") {
47
+ braces += 1;
48
+ }
49
+ else if (character === "}") {
50
+ braces -= 1;
51
+ if (braces < 0)
52
+ return false;
53
+ }
39
54
  }
40
- if (character === ",") {
41
- entries.push(current);
42
- current = "";
43
- continue;
55
+ return quote === null && brackets === 0 && braces === 0;
56
+ }
57
+ function splitInlineList(value) {
58
+ let body = value.trim();
59
+ if (body.startsWith("[") && body.endsWith("]"))
60
+ body = body.slice(1, -1);
61
+ const entries = [];
62
+ let current = "";
63
+ let quote = null;
64
+ let brackets = 0;
65
+ let braces = 0;
66
+ for (let index = 0; index < body.length; index += 1) {
67
+ const character = body[index];
68
+ if (quote !== null) {
69
+ current += character;
70
+ if (character === "\\") {
71
+ const next = body[index + 1];
72
+ if (next !== undefined) {
73
+ current += next;
74
+ index += 1;
75
+ }
76
+ }
77
+ else if (character === quote) {
78
+ quote = null;
79
+ }
80
+ continue;
81
+ }
82
+ if (character === '"' || character === "'") {
83
+ quote = character;
84
+ current += character;
85
+ continue;
86
+ }
87
+ if (character === "\\") {
88
+ current += character;
89
+ const next = body[index + 1];
90
+ if (next !== undefined) {
91
+ current += next;
92
+ index += 1;
93
+ }
94
+ continue;
95
+ }
96
+ if (character === "[")
97
+ brackets += 1;
98
+ if (character === "]")
99
+ brackets -= 1;
100
+ if (character === "{")
101
+ braces += 1;
102
+ if (character === "}")
103
+ braces -= 1;
104
+ if (character === "," && brackets === 0 && braces === 0) {
105
+ entries.push(current);
106
+ current = "";
107
+ continue;
108
+ }
109
+ current += character;
44
110
  }
45
- current += character;
46
- }
47
- entries.push(current);
48
- return entries.map(stripQuotes).filter((entry) => entry.length > 0);
111
+ entries.push(current);
112
+ return entries.map(stripQuotes).filter((entry) => entry.length > 0);
49
113
  }
50
-
51
114
  /**
52
115
  * Read the rule body and its optional path patterns.
53
- *
54
- * @param {string} content - complete rule file text.
55
- * @returns {{paths: string[] | undefined, body: string}} parsed rule.
56
116
  */
57
117
  export function parseRule(content) {
58
- const source = content.startsWith("\uFEFF") ? content.slice(1) : content;
59
- const lines = source.split(/\r?\n/);
60
- if (lines[0]?.trim() !== DELIMITER) return { paths: undefined, body: source };
61
-
62
- let end = -1;
63
- for (let index = 1; index < lines.length; index += 1) {
64
- if (lines[index].trim() === DELIMITER) {
65
- end = index;
66
- break;
118
+ const source = content.startsWith("\uFEFF") ? content.slice(1) : content;
119
+ const lines = source.split(/\r?\n/);
120
+ if (lines[0]?.trim() !== DELIMITER)
121
+ return { body: source };
122
+ let end = -1;
123
+ for (let index = 1; index < lines.length; index += 1) {
124
+ if (lines[index].trim() === DELIMITER) {
125
+ end = index;
126
+ break;
127
+ }
67
128
  }
68
- }
69
- if (end === -1) return { paths: undefined, body: source };
70
-
71
- const frontmatter = lines.slice(1, end);
72
- const body = lines.slice(end + 1).join("\n").replace(/^\n+/u, "");
73
- return { paths: parsePaths(frontmatter), body };
129
+ if (end === -1)
130
+ return { body: source };
131
+ const frontmatter = lines.slice(1, end);
132
+ const body = lines.slice(end + 1).join("\n").replace(/^\n+/u, "");
133
+ const paths = parsePaths(frontmatter);
134
+ return paths === undefined ? { body } : { paths, body };
74
135
  }
75
-
76
136
  function parsePaths(frontmatter) {
77
- const index = frontmatter.findIndex((line) => /^paths\s*:/u.test(line));
78
- if (index === -1) return undefined;
79
- const value = frontmatter[index].replace(/^paths\s*:/u, "").trim();
80
- if (value.length > 0) {
81
- const inline = splitInlineList(value);
82
- return inline.length > 0 ? inline : undefined;
83
- }
84
-
85
- const paths = [];
86
- for (let cursor = index + 1; cursor < frontmatter.length; cursor += 1) {
87
- const line = frontmatter[cursor];
88
- if (line.trim().length === 0 || line.trim().startsWith("#")) continue;
89
- const match = /^\s*-\s+(.*)$/u.exec(line);
90
- if (match === null) break;
91
- const path = stripQuotes(match[1]);
92
- if (path.length > 0) paths.push(path);
93
- }
94
- return paths.length > 0 ? paths : undefined;
137
+ const index = frontmatter.findIndex((line) => /^paths\s*:/u.test(line));
138
+ if (index === -1)
139
+ return undefined;
140
+ const value = frontmatter[index].replace(/^paths\s*:/u, "").trim();
141
+ if (value.length > 0) {
142
+ // A malformed inline list is a YAML parse failure: load the rule without
143
+ // paths instead of scoping it to a pattern that can never match.
144
+ if (!isBalancedInline(value))
145
+ return undefined;
146
+ const inline = splitInlineList(value);
147
+ return inline.length > 0 ? inline : undefined;
148
+ }
149
+ const paths = [];
150
+ for (let cursor = index + 1; cursor < frontmatter.length; cursor += 1) {
151
+ const line = frontmatter[cursor];
152
+ if (line.trim().length === 0 || line.trim().startsWith("#"))
153
+ continue;
154
+ const match = /^\s*-\s+(.*)$/u.exec(line);
155
+ if (match === null)
156
+ break;
157
+ const path = stripQuotes(match[1]);
158
+ if (path.length > 0)
159
+ paths.push(path);
160
+ }
161
+ return paths.length > 0 ? paths : undefined;
95
162
  }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Small glob matcher for rule `paths` patterns.
3
+ *
4
+ * Supports `*`, `**`, `?`, brace expansion (`{a,b}`), and character classes
5
+ * (`[abc]`). Patterns are relative to the rule's base directory and use `/`
6
+ * separators on every platform.
7
+ */
8
+ /**
9
+ * Expand brace alternatives with a hard budget. A pattern over budget is
10
+ * returned unexpanded, so its literal braces match nothing rather than
11
+ * consuming unbounded memory.
12
+ */
13
+ export declare function expandBraces(pattern: string): string[];
14
+ /**
15
+ * Compile one glob pattern into an anchored regular expression.
16
+ */
17
+ export declare function compileGlob(pattern: string): RegExp;
18
+ /**
19
+ * Test one `/`-separated relative path against a glob.
20
+ */
21
+ export declare function matchGlob(path: string, pattern: string): boolean;