@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/README.md +26 -14
- package/lib/config.d.ts +47 -0
- package/lib/config.js +89 -97
- package/lib/guidance.d.ts +9 -0
- package/lib/guidance.js +5 -9
- package/lib/index.d.ts +13 -0
- package/lib/index.js +68 -89
- package/lib/inject.d.ts +36 -0
- package/lib/inject.js +80 -82
- package/lib/memory-frontmatter.d.ts +60 -0
- package/lib/memory-frontmatter.js +169 -102
- package/lib/paths.d.ts +38 -0
- package/lib/paths.js +117 -66
- package/lib/render.d.ts +24 -0
- package/lib/render.js +44 -61
- package/lib/rule-frontmatter.d.ts +15 -0
- package/lib/rule-frontmatter.js +141 -74
- package/lib/rule-glob.d.ts +21 -0
- package/lib/rule-glob.js +149 -124
- package/lib/rules-inject.d.ts +14 -0
- package/lib/rules-inject.js +119 -111
- package/lib/rules.d.ts +62 -0
- package/lib/rules.js +235 -227
- package/lib/store.d.ts +169 -0
- package/lib/store.js +766 -761
- package/lib/tool.d.ts +63 -0
- package/lib/tool.js +224 -224
- package/lib/types.d.ts +122 -0
- package/lib/types.js +1 -0
- package/package.json +14 -7
package/lib/rule-glob.js
CHANGED
|
@@ -5,159 +5,184 @@
|
|
|
5
5
|
* (`[abc]`). Patterns are relative to the rule's base directory and use `/`
|
|
6
6
|
* separators on every platform.
|
|
7
7
|
*/
|
|
8
|
-
|
|
9
8
|
const EXPANSION_BUDGET = 1000;
|
|
10
9
|
const COMPILED_CACHE = new Map();
|
|
11
10
|
const COMPILED_CACHE_LIMIT = 2000;
|
|
12
|
-
|
|
11
|
+
const NEVER = /^(?!)$/u;
|
|
13
12
|
function findBrace(pattern, from) {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
13
|
+
let depth = 0;
|
|
14
|
+
let start = -1;
|
|
15
|
+
for (let index = from; index < pattern.length; index += 1) {
|
|
16
|
+
const character = pattern[index];
|
|
17
|
+
if (character === "\\") {
|
|
18
|
+
index += 1;
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
if (character === "{") {
|
|
22
|
+
if (depth === 0)
|
|
23
|
+
start = index;
|
|
24
|
+
depth += 1;
|
|
25
|
+
}
|
|
26
|
+
else if (character === "}") {
|
|
27
|
+
if (depth > 0) {
|
|
28
|
+
depth -= 1;
|
|
29
|
+
if (depth === 0 && start !== -1)
|
|
30
|
+
return { start, end: index };
|
|
31
|
+
}
|
|
32
|
+
}
|
|
30
33
|
}
|
|
31
|
-
|
|
32
|
-
return undefined;
|
|
34
|
+
return undefined;
|
|
33
35
|
}
|
|
34
|
-
|
|
35
36
|
function splitAlternatives(inner) {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
37
|
+
const parts = [];
|
|
38
|
+
let current = "";
|
|
39
|
+
let depth = 0;
|
|
40
|
+
for (const character of inner) {
|
|
41
|
+
if (character === "{")
|
|
42
|
+
depth += 1;
|
|
43
|
+
if (character === "}")
|
|
44
|
+
depth -= 1;
|
|
45
|
+
if (character === "," && depth === 0) {
|
|
46
|
+
parts.push(current);
|
|
47
|
+
current = "";
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
current += character;
|
|
51
|
+
}
|
|
47
52
|
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
return parts;
|
|
53
|
+
parts.push(current);
|
|
54
|
+
return parts;
|
|
51
55
|
}
|
|
52
|
-
|
|
53
56
|
function expandInto(pattern, from, output) {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
57
|
+
if (output.length >= EXPANSION_BUDGET)
|
|
58
|
+
return;
|
|
59
|
+
const brace = findBrace(pattern, from);
|
|
60
|
+
if (brace === undefined) {
|
|
61
|
+
output.push(pattern);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
const alternatives = splitAlternatives(pattern.slice(brace.start + 1, brace.end));
|
|
65
|
+
if (alternatives.length < 2) {
|
|
66
|
+
expandInto(pattern, brace.end + 1, output);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
const prefix = pattern.slice(0, brace.start);
|
|
70
|
+
const suffix = pattern.slice(brace.end + 1);
|
|
71
|
+
for (const alternative of alternatives) {
|
|
72
|
+
if (output.length >= EXPANSION_BUDGET)
|
|
73
|
+
break;
|
|
74
|
+
expandInto(`${prefix}${alternative}${suffix}`, brace.start, output);
|
|
75
|
+
}
|
|
71
76
|
}
|
|
72
|
-
|
|
73
77
|
/**
|
|
74
78
|
* Expand brace alternatives with a hard budget. A pattern over budget is
|
|
75
79
|
* returned unexpanded, so its literal braces match nothing rather than
|
|
76
80
|
* consuming unbounded memory.
|
|
77
|
-
*
|
|
78
|
-
* @param {string} pattern - glob pattern.
|
|
79
|
-
* @returns {string[]} expanded patterns.
|
|
80
81
|
*/
|
|
81
82
|
export function expandBraces(pattern) {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
83
|
+
const output = [];
|
|
84
|
+
expandInto(pattern, 0, output);
|
|
85
|
+
return output.length > 0 ? output : [pattern];
|
|
85
86
|
}
|
|
86
|
-
|
|
87
87
|
function normalizePattern(pattern) {
|
|
88
|
-
|
|
88
|
+
let value = pattern.replace(/^\.\//u, "");
|
|
89
|
+
if (value.startsWith("/")) {
|
|
90
|
+
return { pattern: value.replace(/^\/+/, ""), anchored: true };
|
|
91
|
+
}
|
|
92
|
+
return { pattern: value, anchored: value.includes("/") };
|
|
93
|
+
}
|
|
94
|
+
function escapeRegex(character) {
|
|
95
|
+
return /[.*+?^${}()|[\]\\]/u.test(character) ? `\\${character}` : character;
|
|
89
96
|
}
|
|
90
|
-
|
|
91
97
|
/**
|
|
92
98
|
* Compile one glob pattern into an anchored regular expression.
|
|
93
|
-
*
|
|
94
|
-
* @param {string} pattern - glob pattern.
|
|
95
|
-
* @returns {RegExp} matcher for `/`-separated relative paths.
|
|
96
99
|
*/
|
|
97
100
|
export function compileGlob(pattern) {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
101
|
+
const cached = COMPILED_CACHE.get(pattern);
|
|
102
|
+
if (cached !== undefined)
|
|
103
|
+
return cached;
|
|
104
|
+
const normalized = normalizePattern(pattern);
|
|
105
|
+
const source = normalized.pattern;
|
|
106
|
+
let expression = normalized.anchored ? "^" : "^(?:.*/)?";
|
|
107
|
+
for (let index = 0; index < source.length; index += 1) {
|
|
108
|
+
const character = source[index];
|
|
109
|
+
if (character === "\\") {
|
|
110
|
+
const next = source[index + 1];
|
|
111
|
+
if (next !== undefined) {
|
|
112
|
+
expression += escapeRegex(next);
|
|
113
|
+
index += 1;
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
expression += "\\\\";
|
|
117
|
+
}
|
|
118
|
+
continue;
|
|
114
119
|
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
120
|
+
if (character === "*") {
|
|
121
|
+
let end = index;
|
|
122
|
+
while (source[end + 1] === "*")
|
|
123
|
+
end += 1;
|
|
124
|
+
if (end > index) {
|
|
125
|
+
if (source[end + 1] === "/") {
|
|
126
|
+
expression += "(?:.*/)?";
|
|
127
|
+
index = end + 1;
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
expression += ".*";
|
|
131
|
+
index = end;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
expression += "[^/]*";
|
|
136
|
+
}
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
if (character === "?") {
|
|
140
|
+
expression += "[^/]";
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
if (character === "[") {
|
|
144
|
+
let close = -1;
|
|
145
|
+
for (let cursor = index + 1; cursor < source.length; cursor += 1) {
|
|
146
|
+
if (source[cursor] === "\\") {
|
|
147
|
+
cursor += 1;
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
if (source[cursor] === "]") {
|
|
151
|
+
close = cursor;
|
|
152
|
+
break;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
if (close === -1) {
|
|
156
|
+
return NEVER;
|
|
157
|
+
}
|
|
158
|
+
let characterClass = source.slice(index + 1, close);
|
|
159
|
+
if (characterClass.startsWith("!"))
|
|
160
|
+
characterClass = `^${characterClass.slice(1)}`;
|
|
161
|
+
expression += `[${characterClass}]`;
|
|
162
|
+
index = close;
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
if (character === "{" || character === "}") {
|
|
166
|
+
expression += `\\${character}`;
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
if ("\\.+^$()|".includes(character)) {
|
|
170
|
+
expression += `\\${character}`;
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
expression += character;
|
|
143
174
|
}
|
|
144
|
-
expression +=
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
return compiled;
|
|
175
|
+
expression += "$";
|
|
176
|
+
const compiled = new RegExp(expression);
|
|
177
|
+
if (COMPILED_CACHE.size >= COMPILED_CACHE_LIMIT)
|
|
178
|
+
COMPILED_CACHE.clear();
|
|
179
|
+
COMPILED_CACHE.set(pattern, compiled);
|
|
180
|
+
return compiled;
|
|
151
181
|
}
|
|
152
|
-
|
|
153
182
|
/**
|
|
154
183
|
* Test one `/`-separated relative path against a glob.
|
|
155
|
-
*
|
|
156
|
-
* @param {string} path - relative path.
|
|
157
|
-
* @param {string} pattern - glob pattern.
|
|
158
|
-
* @returns {boolean} whether the path matches.
|
|
159
184
|
*/
|
|
160
185
|
export function matchGlob(path, pattern) {
|
|
161
|
-
|
|
162
|
-
|
|
186
|
+
const normalized = path.replaceAll("\\", "/").replace(/^\.\//u, "");
|
|
187
|
+
return expandBraces(pattern).some((expanded) => compileGlob(expanded).test(normalized));
|
|
163
188
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ProjectFor } from "./inject.js";
|
|
2
|
+
import type { ResolvedConfig } from "./config.js";
|
|
3
|
+
import type { Logger, PreStepHandler, ToolExecEvent, ToolResultEvent } from "./types.js";
|
|
4
|
+
/**
|
|
5
|
+
* Register rule injection and file-touch tracking.
|
|
6
|
+
*/
|
|
7
|
+
export declare function createRulesFeature({ projectFor, config, logger }: {
|
|
8
|
+
projectFor: ProjectFor;
|
|
9
|
+
config: ResolvedConfig;
|
|
10
|
+
logger?: Logger;
|
|
11
|
+
}): {
|
|
12
|
+
preStep: PreStepHandler;
|
|
13
|
+
toolResult: (exec: ToolExecEvent, result: ToolResultEvent) => void;
|
|
14
|
+
};
|
package/lib/rules-inject.js
CHANGED
|
@@ -2,129 +2,137 @@ import { createUserMessage } from "@deepseek-ai/dsh-llm";
|
|
|
2
2
|
import { isAbsolute, relative, resolve } from "node:path";
|
|
3
3
|
import { enterWithMessage, isSubagent } from "./inject.js";
|
|
4
4
|
import { RULE_MESSAGE_KIND, RuleIndex, renderRules } from "./rules.js";
|
|
5
|
-
|
|
6
5
|
const FILE_TOOLS = new Set(["read", "write", "edit"]);
|
|
7
|
-
|
|
8
6
|
function toPosix(value) {
|
|
9
|
-
|
|
7
|
+
return value.replaceAll("\\", "/");
|
|
10
8
|
}
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
9
|
+
function ruleMatches(rule, touched, cwd, ruleRoot) {
|
|
10
|
+
if (rule.matchers.length === 0)
|
|
11
|
+
return true;
|
|
12
|
+
const base = rule.scope === "user" ? cwd : ruleRoot;
|
|
13
|
+
for (const raw of touched) {
|
|
14
|
+
const absolute = isAbsolute(raw) ? resolve(raw) : resolve(cwd, raw);
|
|
15
|
+
const rel = relative(base, absolute);
|
|
16
|
+
if (rel.length === 0 || rel.startsWith("..") || isAbsolute(rel))
|
|
17
|
+
continue;
|
|
18
|
+
const posix = toPosix(rel);
|
|
19
|
+
if (rule.matchers.some((matcher) => matcher.test(posix)))
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
return false;
|
|
23
23
|
}
|
|
24
|
-
|
|
25
24
|
function visibleRuleDigests(agent, decision) {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
25
|
+
const visible = new Map();
|
|
26
|
+
const collect = (message) => {
|
|
27
|
+
if (message?.source?.kind !== RULE_MESSAGE_KIND)
|
|
28
|
+
return;
|
|
29
|
+
const entries = message.source.rules;
|
|
30
|
+
if (!Array.isArray(entries))
|
|
31
|
+
return;
|
|
32
|
+
for (const entry of entries) {
|
|
33
|
+
if (typeof entry !== "object" || entry === null)
|
|
34
|
+
continue;
|
|
35
|
+
const candidate = entry;
|
|
36
|
+
if (typeof candidate.key === "string" && typeof candidate.digest === "string") {
|
|
37
|
+
visible.set(candidate.key, candidate.digest);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
for (const message of decision.messages)
|
|
42
|
+
collect(message);
|
|
43
|
+
for (const seq of agent.session.surface.nodes) {
|
|
44
|
+
const event = agent.session.eventAt(seq);
|
|
45
|
+
if (event?.type === "user/message")
|
|
46
|
+
collect(event.data);
|
|
35
47
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
const event = agent.session.eventAt(seq);
|
|
40
|
-
if (event?.type === "user/message") collect(event.data);
|
|
41
|
-
}
|
|
42
|
-
for (const message of agent.inbox.nextStep) collect(message);
|
|
43
|
-
return visible;
|
|
48
|
+
for (const message of agent.inbox.nextStep)
|
|
49
|
+
collect(message);
|
|
50
|
+
return visible;
|
|
44
51
|
}
|
|
45
|
-
|
|
46
52
|
function isRulesMessage(message) {
|
|
47
|
-
|
|
53
|
+
return message?.source?.kind === RULE_MESSAGE_KIND;
|
|
48
54
|
}
|
|
49
|
-
|
|
50
55
|
/**
|
|
51
56
|
* Register rule injection and file-touch tracking.
|
|
52
|
-
*
|
|
53
|
-
* @param {object} input - feature wiring.
|
|
54
|
-
* @param {(agent: object, signal: AbortSignal) => Promise<object>} input.projectFor - resolve the session project.
|
|
55
|
-
* @param {object} input.config - normalized plugin configuration.
|
|
56
|
-
* @param {object} [input.logger] - host logger.
|
|
57
|
-
* @returns {{preStep: Function, toolResult: Function}} event listeners.
|
|
58
57
|
*/
|
|
59
58
|
export function createRulesFeature({ projectFor, config, logger }) {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
const preStep = async ({ agent, messages, signal }, next) => {
|
|
86
|
-
const decision = await next();
|
|
87
|
-
if (decision.kind === "reject") return decision;
|
|
88
|
-
if (!config.rules) return decision;
|
|
89
|
-
if (!config.injectInSubagents && isSubagent(agent)) return decision;
|
|
90
|
-
try {
|
|
91
|
-
const project = await projectFor(agent, signal);
|
|
92
|
-
const { rules } = await index.collect({
|
|
93
|
-
cwd: project.cwd,
|
|
94
|
-
projectRoot: project.projectRoot
|
|
95
|
-
});
|
|
96
|
-
if (rules.length === 0) return decision;
|
|
97
|
-
const sessionTouches = touched.get(agent.session) ?? new Set();
|
|
98
|
-
const eligible = rules.filter((rule) =>
|
|
99
|
-
ruleMatches(rule, sessionTouches, project.cwd, project.projectRoot)
|
|
100
|
-
);
|
|
101
|
-
if (eligible.length === 0) return decision;
|
|
102
|
-
const visible = visibleRuleDigests(agent, decision);
|
|
103
|
-
const fresh = eligible.filter((rule) => visible.get(rule.key) !== rule.digest);
|
|
104
|
-
if (fresh.length === 0) return decision;
|
|
105
|
-
const rendered = renderRules(fresh, config.maxRulesBytes);
|
|
106
|
-
if (rendered === undefined) return decision;
|
|
107
|
-
const message = createUserMessage({
|
|
108
|
-
content: [{ type: "text", text: rendered.text }],
|
|
109
|
-
source: {
|
|
110
|
-
kind: RULE_MESSAGE_KIND,
|
|
111
|
-
form: "instructions",
|
|
112
|
-
digest: rendered.digest,
|
|
113
|
-
rules: rendered.rules.map((rule) => ({
|
|
114
|
-
key: rule.key,
|
|
115
|
-
path: rule.path,
|
|
116
|
-
scope: rule.scope,
|
|
117
|
-
digest: rule.digest
|
|
118
|
-
}))
|
|
59
|
+
const index = new RuleIndex({
|
|
60
|
+
userRuleDirs: config.userRuleDirs,
|
|
61
|
+
projectRuleDirs: config.projectRuleDirs,
|
|
62
|
+
maxRuleBytes: config.maxRuleBytes,
|
|
63
|
+
maxRules: config.maxRules,
|
|
64
|
+
maxRulesBytes: config.maxRulesBytes
|
|
65
|
+
});
|
|
66
|
+
const touched = new WeakMap();
|
|
67
|
+
const toolResult = (exec, result) => {
|
|
68
|
+
if (result?.isError)
|
|
69
|
+
return;
|
|
70
|
+
if (!FILE_TOOLS.has(exec?.name ?? ""))
|
|
71
|
+
return;
|
|
72
|
+
const agent = exec.agent;
|
|
73
|
+
if (agent === undefined)
|
|
74
|
+
return;
|
|
75
|
+
if (!config.injectInSubagents && isSubagent(agent))
|
|
76
|
+
return;
|
|
77
|
+
const raw = exec.arguments?.file_path;
|
|
78
|
+
if (typeof raw !== "string" || raw.trim().length === 0)
|
|
79
|
+
return;
|
|
80
|
+
let paths = touched.get(agent.session);
|
|
81
|
+
if (paths === undefined) {
|
|
82
|
+
paths = new Set();
|
|
83
|
+
touched.set(agent.session, paths);
|
|
119
84
|
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
85
|
+
paths.add(raw.trim());
|
|
86
|
+
};
|
|
87
|
+
const preStep = async ({ agent, messages, signal }, next) => {
|
|
88
|
+
const decision = await next();
|
|
89
|
+
if (decision.kind === "reject")
|
|
90
|
+
return decision;
|
|
91
|
+
if (!config.rules)
|
|
92
|
+
return decision;
|
|
93
|
+
if (!config.injectInSubagents && isSubagent(agent))
|
|
94
|
+
return decision;
|
|
95
|
+
try {
|
|
96
|
+
const project = await projectFor(agent, signal);
|
|
97
|
+
const { rules, warnings } = await index.collect({
|
|
98
|
+
cwd: project.cwd,
|
|
99
|
+
projectRoot: project.ruleRoot
|
|
100
|
+
});
|
|
101
|
+
if (rules.length === 0)
|
|
102
|
+
return decision;
|
|
103
|
+
const sessionTouches = touched.get(agent.session) ?? new Set();
|
|
104
|
+
const eligible = rules.filter((rule) => ruleMatches(rule, sessionTouches, project.cwd, project.ruleRoot));
|
|
105
|
+
if (eligible.length === 0)
|
|
106
|
+
return decision;
|
|
107
|
+
const visible = visibleRuleDigests(agent, decision);
|
|
108
|
+
const fresh = eligible.filter((rule) => visible.get(rule.key) !== rule.digest);
|
|
109
|
+
if (fresh.length === 0)
|
|
110
|
+
return decision;
|
|
111
|
+
const rendered = renderRules(fresh, config.maxRulesBytes, warnings);
|
|
112
|
+
if (rendered === undefined)
|
|
113
|
+
return decision;
|
|
114
|
+
const message = createUserMessage({
|
|
115
|
+
content: [{ type: "text", text: rendered.text }],
|
|
116
|
+
source: {
|
|
117
|
+
kind: RULE_MESSAGE_KIND,
|
|
118
|
+
form: "instructions",
|
|
119
|
+
digest: rendered.digest,
|
|
120
|
+
rules: rendered.rules.map((rule) => ({
|
|
121
|
+
key: rule.key,
|
|
122
|
+
path: rule.path,
|
|
123
|
+
scope: rule.scope,
|
|
124
|
+
digest: rule.digest
|
|
125
|
+
}))
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
return enterWithMessage(agent, messages, decision, message, isRulesMessage);
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
if (signal?.aborted)
|
|
132
|
+
return decision;
|
|
133
|
+
logger?.warn?.("auto-memory: could not load rules for this step: %o", error);
|
|
134
|
+
return decision;
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
return { preStep, toolResult };
|
|
130
138
|
}
|
package/lib/rules.d.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { RuleScope } from "./types.js";
|
|
2
|
+
/** Message source kind used to recognize injected rule batches. */
|
|
3
|
+
export declare const RULE_MESSAGE_KIND = "auto-rules";
|
|
4
|
+
export interface Rule {
|
|
5
|
+
key: string;
|
|
6
|
+
path: string;
|
|
7
|
+
scope: RuleScope;
|
|
8
|
+
body: string;
|
|
9
|
+
paths?: string[];
|
|
10
|
+
matchers: RegExp[];
|
|
11
|
+
digest: string;
|
|
12
|
+
bytes: number;
|
|
13
|
+
}
|
|
14
|
+
export interface RuleIndexOptions {
|
|
15
|
+
userRuleDirs: string[];
|
|
16
|
+
projectRuleDirs: string[];
|
|
17
|
+
maxRuleBytes: number;
|
|
18
|
+
maxRules: number;
|
|
19
|
+
maxRulesBytes: number;
|
|
20
|
+
}
|
|
21
|
+
export interface CollectInput {
|
|
22
|
+
cwd: string;
|
|
23
|
+
projectRoot: string;
|
|
24
|
+
}
|
|
25
|
+
export interface CollectResult {
|
|
26
|
+
rules: Rule[];
|
|
27
|
+
warnings: string[];
|
|
28
|
+
}
|
|
29
|
+
/** The subset of a rule that rendering needs. */
|
|
30
|
+
export interface RenderableRule {
|
|
31
|
+
key: string;
|
|
32
|
+
path: string;
|
|
33
|
+
scope: RuleScope;
|
|
34
|
+
body: string;
|
|
35
|
+
digest: string;
|
|
36
|
+
}
|
|
37
|
+
export interface RenderedRules {
|
|
38
|
+
text: string;
|
|
39
|
+
digest: string;
|
|
40
|
+
rules: RenderableRule[];
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Discover and cache rule files from user and project roots.
|
|
44
|
+
*
|
|
45
|
+
* Files are re-stat'ed on every collection; content is read again only when
|
|
46
|
+
* size or mtime changes. Symlinks are followed, which is required for the
|
|
47
|
+
* Home Manager-managed rules under `~/.claude/rules/`.
|
|
48
|
+
*/
|
|
49
|
+
export declare class RuleIndex {
|
|
50
|
+
#private;
|
|
51
|
+
readonly userRuleDirs: string[];
|
|
52
|
+
readonly projectRuleDirs: string[];
|
|
53
|
+
readonly maxRuleBytes: number;
|
|
54
|
+
readonly maxRules: number;
|
|
55
|
+
readonly maxRulesBytes: number;
|
|
56
|
+
constructor({ userRuleDirs, projectRuleDirs, maxRuleBytes, maxRules, maxRulesBytes }: RuleIndexOptions);
|
|
57
|
+
/**
|
|
58
|
+
* Collect the rules visible to one session.
|
|
59
|
+
*/
|
|
60
|
+
collect({ cwd, projectRoot }: CollectInput): Promise<CollectResult>;
|
|
61
|
+
}
|
|
62
|
+
export declare function renderRules(rules: RenderableRule[], maxBytes: number, warnings?: string[]): RenderedRules | undefined;
|