opencode-rules-md 0.8.6 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -9
- package/dist/src/cli/config.d.ts +1 -1
- package/dist/src/cli/config.js.map +1 -1
- package/dist/src/cli/install.d.ts.map +1 -1
- package/dist/src/cli/install.js.map +1 -1
- package/dist/src/cli/main.d.ts.map +1 -1
- package/dist/src/cli/main.js.map +1 -1
- package/dist/src/cli/registry.d.ts.map +1 -1
- package/dist/src/cli/spawn.js.map +1 -1
- package/dist/src/cli/status.d.ts.map +1 -1
- package/dist/src/cli/status.js.map +1 -1
- package/dist/src/cli/uninstall.d.ts +1 -1
- package/dist/src/cli/uninstall.d.ts.map +1 -1
- package/dist/src/cli/uninstall.js.map +1 -1
- package/dist/src/cli/update.d.ts +1 -1
- package/dist/src/cli/update.d.ts.map +1 -1
- package/dist/src/cli/update.js.map +1 -1
- package/dist/src/index.d.ts +2 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/rule-discovery.d.ts.map +1 -1
- package/dist/src/rule-discovery.js +18 -10
- package/dist/src/rule-discovery.js.map +1 -1
- package/dist/src/rule-filter.d.ts +9 -0
- package/dist/src/rule-filter.d.ts.map +1 -1
- package/dist/src/rule-filter.js +48 -11
- package/dist/src/rule-filter.js.map +1 -1
- package/dist/src/rule-metadata.d.ts +2 -0
- package/dist/src/rule-metadata.d.ts.map +1 -1
- package/dist/src/rule-metadata.js +4 -0
- package/dist/src/rule-metadata.js.map +1 -1
- package/dist/src/runtime-context.d.ts +6 -0
- package/dist/src/runtime-context.d.ts.map +1 -1
- package/dist/src/runtime-context.js +16 -0
- package/dist/src/runtime-context.js.map +1 -1
- package/dist/src/runtime.d.ts.map +1 -1
- package/dist/src/session-store.d.ts.map +1 -1
- package/dist/src/session-store.js +1 -0
- package/dist/src/session-store.js.map +1 -1
- package/dist/tui/index.d.ts.map +1 -1
- package/dist/tui/index.js +13 -9
- package/dist/tui/index.js.map +4 -4
- package/package.json +14 -17
- package/src/rule-discovery.ts +22 -10
- package/src/rule-filter.ts +73 -12
- package/src/rule-metadata.ts +8 -0
- package/src/runtime-context.ts +16 -0
- package/src/session-store.ts +1 -0
package/src/rule-filter.ts
CHANGED
|
@@ -59,12 +59,23 @@ export function toolsMatchAvailable(
|
|
|
59
59
|
return requiredTools.some(tool => availableSet.has(tool));
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
/**
|
|
63
|
+
* Rough token estimate using the chars/4 heuristic.
|
|
64
|
+
* No external dependency; accurate enough for budget decisions.
|
|
65
|
+
*/
|
|
66
|
+
export function estimateTokens(text: string): number {
|
|
67
|
+
if (!text) return 0;
|
|
68
|
+
return Math.ceil(text.length / 4);
|
|
69
|
+
}
|
|
70
|
+
|
|
62
71
|
/**
|
|
63
72
|
* Result of reading and formatting rules
|
|
64
73
|
*/
|
|
65
74
|
export interface FilterResult {
|
|
66
75
|
formattedRules: string;
|
|
67
76
|
matchedPaths: string[];
|
|
77
|
+
/** Estimated token count of formattedRules (0 when empty) */
|
|
78
|
+
tokenEstimate: number;
|
|
68
79
|
}
|
|
69
80
|
|
|
70
81
|
/**
|
|
@@ -91,6 +102,8 @@ export interface RuleFilterContext {
|
|
|
91
102
|
os?: string;
|
|
92
103
|
/** Whether running in CI environment */
|
|
93
104
|
ci?: boolean;
|
|
105
|
+
/** Optional hard token cap; lowest-priority rules are dropped when exceeded. */
|
|
106
|
+
maxTokens?: number;
|
|
94
107
|
}
|
|
95
108
|
|
|
96
109
|
/**
|
|
@@ -103,16 +116,26 @@ export async function readAndFormatRules(
|
|
|
103
116
|
context: RuleFilterContext = {}
|
|
104
117
|
): Promise<FilterResult> {
|
|
105
118
|
if (files.length === 0) {
|
|
106
|
-
return { formattedRules: '', matchedPaths: [] };
|
|
119
|
+
return { formattedRules: '', matchedPaths: [], tokenEstimate: 0 };
|
|
107
120
|
}
|
|
108
121
|
|
|
109
|
-
const ruleContents: string[] = [];
|
|
110
|
-
const matchedPaths: string[] = [];
|
|
111
122
|
const availableToolSet =
|
|
112
123
|
context.availableToolIDs && context.availableToolIDs.length > 0
|
|
113
124
|
? new Set(context.availableToolIDs)
|
|
114
125
|
: undefined;
|
|
115
126
|
|
|
127
|
+
// Collect matched entries with priority and token count
|
|
128
|
+
type MatchedEntry = {
|
|
129
|
+
filePath: string;
|
|
130
|
+
relativePath: string;
|
|
131
|
+
strippedContent: string;
|
|
132
|
+
priority: number;
|
|
133
|
+
tokenCount: number;
|
|
134
|
+
index: number;
|
|
135
|
+
};
|
|
136
|
+
const entries: MatchedEntry[] = [];
|
|
137
|
+
let entryIndex = 0;
|
|
138
|
+
|
|
116
139
|
for (const { filePath, relativePath } of files) {
|
|
117
140
|
// Use cached rule data with mtime-based invalidation
|
|
118
141
|
const cachedRule = await getCachedRule(filePath);
|
|
@@ -250,20 +273,58 @@ export async function readAndFormatRules(
|
|
|
250
273
|
);
|
|
251
274
|
}
|
|
252
275
|
|
|
253
|
-
//
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
276
|
+
// Extract priority (default 0 for selection; undefined in metadata means absent)
|
|
277
|
+
const priority = metadata?.priority ?? 0;
|
|
278
|
+
|
|
279
|
+
// Build formatted chunk for token counting
|
|
280
|
+
const formattedChunk = `## ${relativePath}\n\n${strippedContent}`;
|
|
281
|
+
const tokenCount = estimateTokens(formattedChunk);
|
|
282
|
+
|
|
283
|
+
entries.push({ filePath, relativePath, strippedContent, priority, tokenCount, index: entryIndex++ });
|
|
257
284
|
}
|
|
258
285
|
|
|
259
|
-
if (
|
|
260
|
-
return { formattedRules: '', matchedPaths: [] };
|
|
286
|
+
if (entries.length === 0) {
|
|
287
|
+
return { formattedRules: '', matchedPaths: [], tokenEstimate: 0 };
|
|
261
288
|
}
|
|
262
289
|
|
|
290
|
+
// Determine which entries to include based on budget
|
|
291
|
+
const maxTokens = context.maxTokens;
|
|
292
|
+
const hasValidBudget = typeof maxTokens === 'number' && maxTokens > 0 && Number.isFinite(maxTokens);
|
|
293
|
+
|
|
294
|
+
let survivors: MatchedEntry[];
|
|
295
|
+
if (!hasValidBudget) {
|
|
296
|
+
// No budget: use discovery order (existing behavior)
|
|
297
|
+
survivors = entries;
|
|
298
|
+
} else {
|
|
299
|
+
// Valid budget: stable sort by priority desc, index asc
|
|
300
|
+
const sorted = [...entries].sort((a, b) =>
|
|
301
|
+
(b.priority - a.priority) || (a.index - b.index)
|
|
302
|
+
);
|
|
303
|
+
|
|
304
|
+
// Greedy selection: always keep first (≥1 survivor invariant), then add while within budget
|
|
305
|
+
survivors = [sorted[0]];
|
|
306
|
+
let runningTokens = sorted[0].tokenCount;
|
|
307
|
+
for (let i = 1; i < sorted.length; i++) {
|
|
308
|
+
if (runningTokens + sorted[i].tokenCount <= maxTokens) {
|
|
309
|
+
survivors.push(sorted[i]);
|
|
310
|
+
runningTokens += sorted[i].tokenCount;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Build output from survivors
|
|
316
|
+
const ruleContents = survivors.map(
|
|
317
|
+
entry => `## ${entry.relativePath}\n\n${entry.strippedContent}`
|
|
318
|
+
);
|
|
319
|
+
const matchedPaths = survivors.map(entry => entry.filePath);
|
|
320
|
+
|
|
321
|
+
const formattedRules =
|
|
322
|
+
`# OpenCode Rules\n\nPlease follow the following rules:\n\n` +
|
|
323
|
+
ruleContents.join('\n\n---\n\n');
|
|
324
|
+
|
|
263
325
|
return {
|
|
264
|
-
formattedRules
|
|
265
|
-
`# OpenCode Rules\n\nPlease follow the following rules:\n\n` +
|
|
266
|
-
ruleContents.join('\n\n---\n\n'),
|
|
326
|
+
formattedRules,
|
|
267
327
|
matchedPaths,
|
|
328
|
+
tokenEstimate: estimateTokens(formattedRules),
|
|
268
329
|
};
|
|
269
330
|
}
|
package/src/rule-metadata.ts
CHANGED
|
@@ -19,6 +19,8 @@ export interface RuleMetadata {
|
|
|
19
19
|
os?: string[];
|
|
20
20
|
ci?: boolean;
|
|
21
21
|
match?: 'any' | 'all';
|
|
22
|
+
/** Higher priority is kept first when a token budget is enforced. Default 0. */
|
|
23
|
+
priority?: number;
|
|
22
24
|
}
|
|
23
25
|
|
|
24
26
|
/**
|
|
@@ -36,6 +38,7 @@ interface ParsedFrontmatter {
|
|
|
36
38
|
os?: unknown;
|
|
37
39
|
ci?: unknown;
|
|
38
40
|
match?: unknown;
|
|
41
|
+
priority?: unknown;
|
|
39
42
|
}
|
|
40
43
|
|
|
41
44
|
/** Field names in ParsedFrontmatter that are string arrays */
|
|
@@ -129,6 +132,11 @@ export function parseRuleMetadata(content: string): RuleMetadata | undefined {
|
|
|
129
132
|
metadata.match = parsed.match;
|
|
130
133
|
}
|
|
131
134
|
|
|
135
|
+
// Extract priority (finite numbers only; non-finite or absent becomes undefined, treated as 0 downstream)
|
|
136
|
+
if (typeof parsed.priority === 'number' && Number.isFinite(parsed.priority)) {
|
|
137
|
+
metadata.priority = parsed.priority;
|
|
138
|
+
}
|
|
139
|
+
|
|
132
140
|
// Return metadata only if it has content
|
|
133
141
|
return Object.keys(metadata).length > 0 ? metadata : undefined;
|
|
134
142
|
} catch (error) {
|
package/src/runtime-context.ts
CHANGED
|
@@ -62,6 +62,18 @@ export function detectCiEnvironment(): boolean {
|
|
|
62
62
|
);
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
/**
|
|
66
|
+
* Resolve the active token budget from the OPENCODE_RULES_MAX_TOKENS env var.
|
|
67
|
+
* Returns undefined when the env var is absent, empty, zero, negative, or non-finite.
|
|
68
|
+
* Positive finite numbers enable budget enforcement.
|
|
69
|
+
*/
|
|
70
|
+
export function resolveMaxTokens(): number | undefined {
|
|
71
|
+
const raw = process.env.OPENCODE_RULES_MAX_TOKENS;
|
|
72
|
+
if (!raw) return undefined;
|
|
73
|
+
const n = Number(raw);
|
|
74
|
+
return Number.isFinite(n) && n > 0 ? n : undefined;
|
|
75
|
+
}
|
|
76
|
+
|
|
65
77
|
/**
|
|
66
78
|
* Build the filter context object used for rule matching.
|
|
67
79
|
* Assembles runtime information from various sources.
|
|
@@ -125,6 +137,10 @@ export async function buildFilterContext(
|
|
|
125
137
|
if (gitBranch !== undefined) {
|
|
126
138
|
context.gitBranch = gitBranch;
|
|
127
139
|
}
|
|
140
|
+
const maxTokens = resolveMaxTokens();
|
|
141
|
+
if (maxTokens !== undefined) {
|
|
142
|
+
context.maxTokens = maxTokens;
|
|
143
|
+
}
|
|
128
144
|
|
|
129
145
|
debugLog(
|
|
130
146
|
`Filter context: model=${modelID ?? 'none'}, agent=${agentType ?? 'none'}, ` +
|