opencode-rules-md 0.6.5

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 (127) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +650 -0
  3. package/dist/cli.mjs +374 -0
  4. package/dist/src/active-rules-state.d.ts +12 -0
  5. package/dist/src/active-rules-state.d.ts.map +1 -0
  6. package/dist/src/active-rules-state.js +118 -0
  7. package/dist/src/active-rules-state.js.map +1 -0
  8. package/dist/src/cli/config.d.ts +62 -0
  9. package/dist/src/cli/config.d.ts.map +1 -0
  10. package/dist/src/cli/config.js +246 -0
  11. package/dist/src/cli/config.js.map +1 -0
  12. package/dist/src/cli/install.d.ts +21 -0
  13. package/dist/src/cli/install.d.ts.map +1 -0
  14. package/dist/src/cli/install.js +63 -0
  15. package/dist/src/cli/install.js.map +1 -0
  16. package/dist/src/cli/main.d.ts +19 -0
  17. package/dist/src/cli/main.d.ts.map +1 -0
  18. package/dist/src/cli/main.js +135 -0
  19. package/dist/src/cli/main.js.map +1 -0
  20. package/dist/src/cli/real-fs.d.ts +26 -0
  21. package/dist/src/cli/real-fs.d.ts.map +1 -0
  22. package/dist/src/cli/real-fs.js +42 -0
  23. package/dist/src/cli/real-fs.js.map +1 -0
  24. package/dist/src/cli/status.d.ts +19 -0
  25. package/dist/src/cli/status.d.ts.map +1 -0
  26. package/dist/src/cli/status.js +58 -0
  27. package/dist/src/cli/status.js.map +1 -0
  28. package/dist/src/debug.d.ts +3 -0
  29. package/dist/src/debug.d.ts.map +1 -0
  30. package/dist/src/debug.js +8 -0
  31. package/dist/src/debug.js.map +1 -0
  32. package/dist/src/git-branch.d.ts +2 -0
  33. package/dist/src/git-branch.d.ts.map +1 -0
  34. package/dist/src/git-branch.js +30 -0
  35. package/dist/src/git-branch.js.map +1 -0
  36. package/dist/src/index.d.ts +26 -0
  37. package/dist/src/index.d.ts.map +1 -0
  38. package/dist/src/index.js +55 -0
  39. package/dist/src/index.js.map +1 -0
  40. package/dist/src/mcp-tools.d.ts +8 -0
  41. package/dist/src/mcp-tools.d.ts.map +1 -0
  42. package/dist/src/mcp-tools.js +18 -0
  43. package/dist/src/mcp-tools.js.map +1 -0
  44. package/dist/src/message-context.d.ts +55 -0
  45. package/dist/src/message-context.d.ts.map +1 -0
  46. package/dist/src/message-context.js +110 -0
  47. package/dist/src/message-context.js.map +1 -0
  48. package/dist/src/message-paths.d.ts +34 -0
  49. package/dist/src/message-paths.d.ts.map +1 -0
  50. package/dist/src/message-paths.js +112 -0
  51. package/dist/src/message-paths.js.map +1 -0
  52. package/dist/src/project-fingerprint.d.ts +2 -0
  53. package/dist/src/project-fingerprint.d.ts.map +1 -0
  54. package/dist/src/project-fingerprint.js +61 -0
  55. package/dist/src/project-fingerprint.js.map +1 -0
  56. package/dist/src/rule-discovery.d.ts +49 -0
  57. package/dist/src/rule-discovery.d.ts.map +1 -0
  58. package/dist/src/rule-discovery.js +143 -0
  59. package/dist/src/rule-discovery.js.map +1 -0
  60. package/dist/src/rule-filter.d.ts +61 -0
  61. package/dist/src/rule-filter.d.ts.map +1 -0
  62. package/dist/src/rule-filter.js +180 -0
  63. package/dist/src/rule-filter.js.map +1 -0
  64. package/dist/src/rule-metadata.d.ts +29 -0
  65. package/dist/src/rule-metadata.d.ts.map +1 -0
  66. package/dist/src/rule-metadata.js +100 -0
  67. package/dist/src/rule-metadata.js.map +1 -0
  68. package/dist/src/runtime-chat.d.ts +34 -0
  69. package/dist/src/runtime-chat.d.ts.map +1 -0
  70. package/dist/src/runtime-chat.js +39 -0
  71. package/dist/src/runtime-chat.js.map +1 -0
  72. package/dist/src/runtime-context.d.ts +25 -0
  73. package/dist/src/runtime-context.d.ts.map +1 -0
  74. package/dist/src/runtime-context.js +109 -0
  75. package/dist/src/runtime-context.js.map +1 -0
  76. package/dist/src/runtime.d.ts +31 -0
  77. package/dist/src/runtime.d.ts.map +1 -0
  78. package/dist/src/runtime.js +238 -0
  79. package/dist/src/runtime.js.map +1 -0
  80. package/dist/src/session-store.d.ts +34 -0
  81. package/dist/src/session-store.d.ts.map +1 -0
  82. package/dist/src/session-store.js +90 -0
  83. package/dist/src/session-store.js.map +1 -0
  84. package/dist/src/utils.d.ts +15 -0
  85. package/dist/src/utils.d.ts.map +1 -0
  86. package/dist/src/utils.js +19 -0
  87. package/dist/src/utils.js.map +1 -0
  88. package/dist/tui/data/rules.d.ts +63 -0
  89. package/dist/tui/data/rules.d.ts.map +1 -0
  90. package/dist/tui/data/rules.js +173 -0
  91. package/dist/tui/data/rules.js.map +1 -0
  92. package/dist/tui/index.d.ts +8 -0
  93. package/dist/tui/index.d.ts.map +1 -0
  94. package/dist/tui/index.js +13 -0
  95. package/dist/tui/index.js.map +1 -0
  96. package/dist/tui/slots/sidebar-content.d.ts +11 -0
  97. package/dist/tui/slots/sidebar-content.d.ts.map +1 -0
  98. package/dist/tui/slots/sidebar-content.js +150 -0
  99. package/dist/tui/slots/sidebar-content.js.map +1 -0
  100. package/package.json +101 -0
  101. package/src/active-rules-state.ts +166 -0
  102. package/src/cli/config.ts +312 -0
  103. package/src/cli/install.ts +94 -0
  104. package/src/cli/main.ts +167 -0
  105. package/src/cli/real-fs.ts +65 -0
  106. package/src/cli/status.ts +80 -0
  107. package/src/debug.ts +9 -0
  108. package/src/git-branch.ts +37 -0
  109. package/src/index.ts +68 -0
  110. package/src/mcp-tools.ts +24 -0
  111. package/src/message-context.ts +139 -0
  112. package/src/message-paths.ts +153 -0
  113. package/src/project-fingerprint.ts +72 -0
  114. package/src/rule-discovery.ts +205 -0
  115. package/src/rule-filter.ts +269 -0
  116. package/src/rule-metadata.ts +161 -0
  117. package/src/runtime-chat.ts +68 -0
  118. package/src/runtime-context.ts +136 -0
  119. package/src/runtime.ts +369 -0
  120. package/src/session-store.ts +129 -0
  121. package/src/utils.ts +36 -0
  122. package/tui/.atl/.skill-registry.cache.json +3 -0
  123. package/tui/.atl/skill-registry.md +57 -0
  124. package/tui/data/rules.ts +230 -0
  125. package/tui/index.tsx +23 -0
  126. package/tui/slots/sidebar-content.tsx +364 -0
  127. package/tui/types/opencode-plugin-tui.d.ts +78 -0
@@ -0,0 +1,161 @@
1
+ /**
2
+ * Rule metadata parsing and frontmatter extraction
3
+ */
4
+
5
+ const { parse: parseYaml } = await import('yaml');
6
+
7
+ /**
8
+ * Metadata extracted from .mdc file frontmatter
9
+ */
10
+ export interface RuleMetadata {
11
+ globs?: string[];
12
+ keywords?: string[];
13
+ tools?: string[];
14
+ model?: string[];
15
+ agent?: string[];
16
+ command?: string[];
17
+ project?: string[];
18
+ branch?: string[];
19
+ os?: string[];
20
+ ci?: boolean;
21
+ match?: 'any' | 'all';
22
+ }
23
+
24
+ /**
25
+ * Raw parsed YAML frontmatter structure
26
+ */
27
+ interface ParsedFrontmatter {
28
+ globs?: unknown;
29
+ keywords?: unknown;
30
+ tools?: unknown;
31
+ model?: unknown;
32
+ agent?: unknown;
33
+ command?: unknown;
34
+ project?: unknown;
35
+ branch?: unknown;
36
+ os?: unknown;
37
+ ci?: unknown;
38
+ match?: unknown;
39
+ }
40
+
41
+ /** Field names in ParsedFrontmatter that are string arrays */
42
+ type StringArrayField =
43
+ | 'globs'
44
+ | 'keywords'
45
+ | 'tools'
46
+ | 'model'
47
+ | 'agent'
48
+ | 'command'
49
+ | 'project'
50
+ | 'branch'
51
+ | 'os';
52
+
53
+ /**
54
+ * Extract and normalize a string array from parsed frontmatter.
55
+ * Filters non-strings, trims whitespace, and removes empty values.
56
+ *
57
+ * @param value - Raw value from parsed YAML (may be array or undefined)
58
+ * @returns Normalized string array, or undefined if empty after filtering
59
+ */
60
+ function extractStringArray(value: unknown): string[] | undefined {
61
+ if (!Array.isArray(value)) {
62
+ return undefined;
63
+ }
64
+ const result = value
65
+ .filter((v): v is string => typeof v === 'string')
66
+ .map(v => v.trim())
67
+ .filter(v => v.length > 0);
68
+ return result.length > 0 ? result : undefined;
69
+ }
70
+
71
+ /**
72
+ * Parse YAML metadata from rule file content using the yaml package.
73
+ * Extracts frontmatter (---) and returns metadata object.
74
+ */
75
+ export function parseRuleMetadata(content: string): RuleMetadata | undefined {
76
+ // Check if content starts with frontmatter
77
+ if (!content.startsWith('---')) {
78
+ return undefined;
79
+ }
80
+
81
+ // Find the closing --- marker
82
+ const endIndex = content.indexOf('---', 3);
83
+ if (endIndex === -1) {
84
+ return undefined;
85
+ }
86
+
87
+ // Extract the YAML frontmatter
88
+ const frontmatter = content.substring(3, endIndex).trim();
89
+ if (!frontmatter) {
90
+ return undefined;
91
+ }
92
+
93
+ try {
94
+ // Parse YAML using the yaml package
95
+ const parsed = parseYaml(frontmatter) as ParsedFrontmatter | null;
96
+ if (!parsed || typeof parsed !== 'object') {
97
+ return undefined;
98
+ }
99
+
100
+ const metadata: RuleMetadata = {};
101
+
102
+ // Array fields to extract using shared helper
103
+ const arrayFields: StringArrayField[] = [
104
+ 'globs',
105
+ 'keywords',
106
+ 'tools',
107
+ 'model',
108
+ 'agent',
109
+ 'command',
110
+ 'project',
111
+ 'branch',
112
+ 'os',
113
+ ];
114
+
115
+ for (const field of arrayFields) {
116
+ const extracted = extractStringArray(parsed[field]);
117
+ if (extracted) {
118
+ metadata[field] = extracted;
119
+ }
120
+ }
121
+
122
+ // Extract ci boolean (only if strictly boolean)
123
+ if (typeof parsed.ci === 'boolean') {
124
+ metadata.ci = parsed.ci;
125
+ }
126
+
127
+ // Extract match (normalize to 'any' | 'all' only)
128
+ if (parsed.match === 'any' || parsed.match === 'all') {
129
+ metadata.match = parsed.match;
130
+ }
131
+
132
+ // Return metadata only if it has content
133
+ return Object.keys(metadata).length > 0 ? metadata : undefined;
134
+ } catch (error) {
135
+ // Log warning for YAML parsing errors
136
+ const message = error instanceof Error ? error.message : String(error);
137
+ console.warn(
138
+ `[opencode-rules-md] Warning: Failed to parse YAML frontmatter: ${message}`
139
+ );
140
+ return undefined;
141
+ }
142
+ }
143
+
144
+ /**
145
+ * Strip YAML frontmatter from rule content
146
+ */
147
+ export function stripFrontmatter(content: string): string {
148
+ // Check if content starts with frontmatter
149
+ if (!content.startsWith('---')) {
150
+ return content;
151
+ }
152
+
153
+ // Find the closing --- marker
154
+ const endIndex = content.indexOf('---', 3);
155
+ if (endIndex === -1) {
156
+ return content;
157
+ }
158
+
159
+ // Return content after the closing marker, trimming leading newline
160
+ return content.substring(endIndex + 3).trimStart();
161
+ }
@@ -0,0 +1,68 @@
1
+ import { extractTextFromParts } from './message-context.js';
2
+ import type { SessionStore } from './session-store.js';
3
+ import type { DebugLog } from './debug.js';
4
+
5
+ export interface ChatMessageInput {
6
+ sessionID?: string;
7
+ model?: { modelID?: string };
8
+ agent?: string;
9
+ }
10
+
11
+ export interface ChatMessageOutput {
12
+ message?: { role?: string };
13
+ parts?: Array<{ type?: string; text?: string; synthetic?: boolean }>;
14
+ }
15
+
16
+ /**
17
+ * Extract user prompt text from chat message parts.
18
+ * Returns empty string if no text parts found.
19
+ */
20
+ export function extractUserPromptFromParts(
21
+ parts:
22
+ | Array<{ type?: string; text?: string; synthetic?: boolean }>
23
+ | undefined
24
+ ): string {
25
+ if (!parts) return '';
26
+ return extractTextFromParts(parts);
27
+ }
28
+
29
+ /**
30
+ * Handle incoming chat messages to update session state.
31
+ * Captures user prompts, model IDs, and agent types.
32
+ */
33
+ export function handleChatMessage(
34
+ input: ChatMessageInput,
35
+ output: ChatMessageOutput,
36
+ sessionStore: SessionStore,
37
+ debugLog: DebugLog
38
+ ): void {
39
+ const sessionID = input?.sessionID;
40
+ if (!sessionID) {
41
+ debugLog('No sessionID in chat.message hook input');
42
+ return;
43
+ }
44
+
45
+ if (output?.message?.role !== 'user') {
46
+ return;
47
+ }
48
+
49
+ const userPrompt = extractUserPromptFromParts(output.parts);
50
+
51
+ sessionStore.upsert(sessionID, state => {
52
+ if (userPrompt) {
53
+ state.lastUserPrompt = userPrompt;
54
+ state.rulesInjected = false;
55
+ }
56
+
57
+ if (input.model?.modelID) {
58
+ state.lastModelID = input.model.modelID;
59
+ }
60
+ if (input.agent) {
61
+ state.lastAgentType = input.agent;
62
+ }
63
+ });
64
+
65
+ debugLog(
66
+ `Updated session ${sessionID} from chat.message (model=${input.model?.modelID ?? 'none'}, agent=${input.agent ?? 'none'})`
67
+ );
68
+ }
@@ -0,0 +1,136 @@
1
+ import { extractSlashCommand } from './message-context.js';
2
+ import { detectProjectTags } from './project-fingerprint.js';
3
+ import { getGitBranch } from './git-branch.js';
4
+ import type { RuleFilterContext } from './rule-filter.js';
5
+ import type { DebugLog } from './debug.js';
6
+
7
+ export interface BuildFilterContextOptions {
8
+ contextFilePaths: string[];
9
+ userPrompt: string | undefined;
10
+ availableToolIDs: string[];
11
+ modelID: string | undefined;
12
+ agentType: string | undefined;
13
+ }
14
+
15
+ /**
16
+ * Parse an env variable value semantically: 'false', '0', '' => false; other non-empty => true.
17
+ * Returns undefined if the variable is not set.
18
+ */
19
+ function parseEnvBoolean(value: string | undefined): boolean | undefined {
20
+ if (value === undefined) return undefined;
21
+ if (value === '') return false;
22
+ const lower = value.toLowerCase();
23
+ if (lower === 'false' || lower === '0') return false;
24
+ return true;
25
+ }
26
+
27
+ /**
28
+ * Check if a string value represents a truthy CI environment variable.
29
+ * Treats 'false', '0', and empty strings as falsy; other non-empty values as truthy.
30
+ */
31
+ function isTruthyEnvValue(value: string | undefined): boolean {
32
+ return parseEnvBoolean(value) === true;
33
+ }
34
+
35
+ /**
36
+ * Detect if running in a CI environment by checking common CI environment variables.
37
+ *
38
+ * If process.env.CI is explicitly set, it is treated as authoritative:
39
+ * - CI='false' or CI='0' or CI='' => return false (no provider var fallback)
40
+ * - CI='true' or CI='1' or any truthy value => return true
41
+ *
42
+ * If process.env.CI is not set (undefined), fall back to provider-specific detection.
43
+ */
44
+ export function detectCiEnvironment(): boolean {
45
+ const env = process.env;
46
+
47
+ const ciExplicit = parseEnvBoolean(env.CI);
48
+ if (ciExplicit !== undefined) {
49
+ return ciExplicit;
50
+ }
51
+
52
+ return (
53
+ isTruthyEnvValue(env.CONTINUOUS_INTEGRATION) ||
54
+ isTruthyEnvValue(env.BUILD_NUMBER) ||
55
+ isTruthyEnvValue(env.GITHUB_ACTIONS) ||
56
+ isTruthyEnvValue(env.GITLAB_CI) ||
57
+ isTruthyEnvValue(env.CIRCLECI) ||
58
+ isTruthyEnvValue(env.TRAVIS) ||
59
+ isTruthyEnvValue(env.JENKINS_URL) ||
60
+ isTruthyEnvValue(env.BUILDKITE) ||
61
+ isTruthyEnvValue(env.TEAMCITY_VERSION)
62
+ );
63
+ }
64
+
65
+ /**
66
+ * Build the filter context object used for rule matching.
67
+ * Assembles runtime information from various sources.
68
+ */
69
+ export async function buildFilterContext(
70
+ opts: BuildFilterContextOptions,
71
+ projectDirectory: string,
72
+ debugLog: DebugLog
73
+ ): Promise<RuleFilterContext> {
74
+ const { contextFilePaths, userPrompt, availableToolIDs, modelID, agentType } =
75
+ opts;
76
+
77
+ const command = extractSlashCommand(userPrompt);
78
+
79
+ let projectTags: string[] | undefined;
80
+ try {
81
+ projectTags = await detectProjectTags(projectDirectory);
82
+ if (projectTags.length === 0) {
83
+ projectTags = undefined;
84
+ }
85
+ } catch {
86
+ projectTags = undefined;
87
+ }
88
+
89
+ let gitBranch: string | undefined;
90
+ try {
91
+ gitBranch = await getGitBranch(projectDirectory);
92
+ } catch {
93
+ gitBranch = undefined;
94
+ }
95
+
96
+ const os = process.platform;
97
+ const ci = detectCiEnvironment();
98
+
99
+ const context: RuleFilterContext = {
100
+ os,
101
+ ci,
102
+ };
103
+
104
+ if (contextFilePaths.length > 0) {
105
+ context.contextFilePaths = contextFilePaths;
106
+ }
107
+ if (userPrompt !== undefined) {
108
+ context.userPrompt = userPrompt;
109
+ }
110
+ if (availableToolIDs.length > 0) {
111
+ context.availableToolIDs = availableToolIDs;
112
+ }
113
+ if (modelID !== undefined) {
114
+ context.modelID = modelID;
115
+ }
116
+ if (agentType !== undefined) {
117
+ context.agentType = agentType;
118
+ }
119
+ if (command !== undefined) {
120
+ context.command = command;
121
+ }
122
+ if (projectTags !== undefined) {
123
+ context.projectTags = projectTags;
124
+ }
125
+ if (gitBranch !== undefined) {
126
+ context.gitBranch = gitBranch;
127
+ }
128
+
129
+ debugLog(
130
+ `Filter context: model=${modelID ?? 'none'}, agent=${agentType ?? 'none'}, ` +
131
+ `command=${command ?? 'none'}, branch=${gitBranch ?? 'none'}, ` +
132
+ `os=${os}, ci=${ci}, projectTags=${projectTags?.join(',') ?? 'none'}`
133
+ );
134
+
135
+ return context;
136
+ }