@oh-my-pi/pi-coding-agent 12.8.2 → 12.10.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.
Files changed (43) hide show
  1. package/CHANGELOG.md +57 -0
  2. package/package.json +7 -7
  3. package/src/capability/rule.ts +173 -5
  4. package/src/cli/args.ts +3 -0
  5. package/src/cli/update-cli.ts +1 -66
  6. package/src/commands/launch.ts +3 -0
  7. package/src/config/model-registry.ts +300 -21
  8. package/src/config/model-resolver.ts +4 -4
  9. package/src/config/settings-schema.ts +12 -0
  10. package/src/discovery/agents.ts +175 -12
  11. package/src/discovery/builtin.ts +3 -13
  12. package/src/discovery/cline.ts +4 -45
  13. package/src/discovery/cursor.ts +2 -29
  14. package/src/discovery/helpers.ts +43 -0
  15. package/src/discovery/index.ts +1 -0
  16. package/src/discovery/opencode.ts +394 -0
  17. package/src/discovery/windsurf.ts +5 -44
  18. package/src/export/ttsr.ts +324 -54
  19. package/src/extensibility/custom-tools/wrapper.ts +1 -11
  20. package/src/internal-urls/index.ts +4 -2
  21. package/src/internal-urls/memory-protocol.ts +133 -0
  22. package/src/internal-urls/router.ts +4 -2
  23. package/src/internal-urls/skill-protocol.ts +1 -1
  24. package/src/internal-urls/types.ts +6 -2
  25. package/src/main.ts +5 -0
  26. package/src/memories/index.ts +6 -13
  27. package/src/modes/components/settings-defs.ts +6 -0
  28. package/src/modes/components/status-line/segments.ts +3 -2
  29. package/src/modes/rpc/rpc-client.ts +16 -0
  30. package/src/prompts/memories/consolidation.md +1 -1
  31. package/src/prompts/memories/read_path.md +4 -4
  32. package/src/prompts/memories/stage_one_input.md +1 -2
  33. package/src/prompts/tools/bash.md +10 -23
  34. package/src/prompts/tools/read.md +2 -0
  35. package/src/sdk.ts +25 -10
  36. package/src/session/agent-session.ts +252 -44
  37. package/src/session/session-manager.ts +79 -36
  38. package/src/tools/bash-skill-urls.ts +177 -0
  39. package/src/tools/bash.ts +7 -1
  40. package/src/tools/fetch.ts +6 -2
  41. package/src/tools/index.ts +2 -2
  42. package/src/tools/output-meta.ts +49 -42
  43. package/src/tools/read.ts +2 -2
@@ -10,8 +10,7 @@ import { readDirEntries, readFile } from "../capability/fs";
10
10
  import type { Rule } from "../capability/rule";
11
11
  import { ruleCapability } from "../capability/rule";
12
12
  import type { LoadContext, LoadResult } from "../capability/types";
13
- import { parseFrontmatter } from "../utils/frontmatter";
14
- import { createSourceMeta, loadFilesFromDir } from "./helpers";
13
+ import { buildRuleFromMarkdown, createSourceMeta, loadFilesFromDir } from "./helpers";
15
14
 
16
15
  const PROVIDER_ID = "cline";
17
16
  const DISPLAY_NAME = "Cline";
@@ -53,29 +52,8 @@ async function loadRules(ctx: LoadContext): Promise<LoadResult<Rule>> {
53
52
  // Directory format: load all *.md files
54
53
  const result = await loadFilesFromDir(ctx, found.path, PROVIDER_ID, "project", {
55
54
  extensions: ["md"],
56
- transform: (name, content, path, source) => {
57
- const { frontmatter, body } = parseFrontmatter(content, { source: path });
58
- const ruleName = name.replace(/\.md$/, "");
59
-
60
- // Parse globs (can be array or single string)
61
- let globs: string[] | undefined;
62
- if (Array.isArray(frontmatter.globs)) {
63
- globs = frontmatter.globs.filter((g): g is string => typeof g === "string");
64
- } else if (typeof frontmatter.globs === "string") {
65
- globs = [frontmatter.globs];
66
- }
67
-
68
- return {
69
- name: ruleName,
70
- path,
71
- content: body,
72
- globs,
73
- alwaysApply: typeof frontmatter.alwaysApply === "boolean" ? frontmatter.alwaysApply : undefined,
74
- description: typeof frontmatter.description === "string" ? frontmatter.description : undefined,
75
- ttsrTrigger: typeof frontmatter.ttsr_trigger === "string" ? frontmatter.ttsr_trigger : undefined,
76
- _source: source,
77
- };
78
- },
55
+ transform: (name, content, path, source) =>
56
+ buildRuleFromMarkdown(name, content, path, source, { stripNamePattern: /\.md$/ }),
79
57
  });
80
58
 
81
59
  items.push(...result.items);
@@ -88,27 +66,8 @@ async function loadRules(ctx: LoadContext): Promise<LoadResult<Rule>> {
88
66
  return { items, warnings };
89
67
  }
90
68
 
91
- const { frontmatter, body } = parseFrontmatter(content, { source: found.path });
92
69
  const source = createSourceMeta(PROVIDER_ID, found.path, "project");
93
-
94
- // Parse globs (can be array or single string)
95
- let globs: string[] | undefined;
96
- if (Array.isArray(frontmatter.globs)) {
97
- globs = frontmatter.globs.filter((g): g is string => typeof g === "string");
98
- } else if (typeof frontmatter.globs === "string") {
99
- globs = [frontmatter.globs];
100
- }
101
-
102
- items.push({
103
- name: "clinerules",
104
- path: found.path,
105
- content: body,
106
- globs,
107
- alwaysApply: typeof frontmatter.alwaysApply === "boolean" ? frontmatter.alwaysApply : undefined,
108
- description: typeof frontmatter.description === "string" ? frontmatter.description : undefined,
109
- ttsrTrigger: typeof frontmatter.ttsr_trigger === "string" ? frontmatter.ttsr_trigger : undefined,
110
- _source: source,
111
- });
70
+ items.push(buildRuleFromMarkdown("clinerules.md", content, found.path, source, { ruleName: "clinerules" }));
112
71
  }
113
72
 
114
73
  return { items, warnings };
@@ -21,8 +21,8 @@ import { ruleCapability } from "../capability/rule";
21
21
  import type { Settings } from "../capability/settings";
22
22
  import { settingsCapability } from "../capability/settings";
23
23
  import type { LoadContext, LoadResult, SourceMeta } from "../capability/types";
24
- import { parseFrontmatter } from "../utils/frontmatter";
25
24
  import {
25
+ buildRuleFromMarkdown,
26
26
  createSourceMeta,
27
27
  expandEnvVarsDeep,
28
28
  getProjectPath,
@@ -137,34 +137,7 @@ async function loadRules(ctx: LoadContext): Promise<LoadResult<Rule>> {
137
137
  }
138
138
 
139
139
  function transformMDCRule(name: string, content: string, path: string, source: SourceMeta): Rule {
140
- const { frontmatter, body } = parseFrontmatter(content, { source: path });
141
-
142
- // Extract frontmatter fields
143
- const description = typeof frontmatter.description === "string" ? frontmatter.description : undefined;
144
- const alwaysApply = frontmatter.alwaysApply === true;
145
- const ttsrTrigger = typeof frontmatter.ttsr_trigger === "string" ? frontmatter.ttsr_trigger : undefined;
146
-
147
- // Parse globs (can be array or single string)
148
- let globs: string[] | undefined;
149
- if (Array.isArray(frontmatter.globs)) {
150
- globs = frontmatter.globs.filter((g): g is string => typeof g === "string");
151
- } else if (typeof frontmatter.globs === "string") {
152
- globs = [frontmatter.globs];
153
- }
154
-
155
- // Derive name from filename (strip extension)
156
- const ruleName = name.replace(/\.(mdc|md)$/, "");
157
-
158
- return {
159
- name: ruleName,
160
- path,
161
- content: body,
162
- description,
163
- alwaysApply,
164
- globs,
165
- ttsrTrigger,
166
- _source: source,
167
- };
140
+ return buildRuleFromMarkdown(name, content, path, source, { stripNamePattern: /\.(mdc|md)$/ });
168
141
  }
169
142
 
170
143
  // =============================================================================
@@ -7,6 +7,7 @@ import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
7
7
  import { FileType, glob } from "@oh-my-pi/pi-natives";
8
8
  import { CONFIG_DIR_NAME } from "@oh-my-pi/pi-utils/dirs";
9
9
  import { readFile } from "../capability/fs";
10
+ import { parseRuleConditionAndScope, type Rule, type RuleFrontmatter } from "../capability/rule";
10
11
  import type { Skill, SkillFrontmatter } from "../capability/skill";
11
12
  import type { LoadContext, LoadResult, SourceMeta } from "../capability/types";
12
13
  import { parseFrontmatter } from "../utils/frontmatter";
@@ -60,6 +61,11 @@ export const SOURCE_PATHS = {
60
61
  userAgent: ".gemini",
61
62
  projectDir: ".gemini",
62
63
  },
64
+ opencode: {
65
+ userBase: ".config/opencode",
66
+ userAgent: ".config/opencode",
67
+ projectDir: ".opencode",
68
+ },
63
69
  cursor: {
64
70
  userBase: ".cursor",
65
71
  userAgent: ".cursor",
@@ -158,6 +164,43 @@ export function parseArrayOrCSV(value: unknown): string[] | undefined {
158
164
  return undefined;
159
165
  }
160
166
 
167
+ /**
168
+ * Build a canonical rule item from a markdown/markdown-frontmatter document.
169
+ */
170
+ export function buildRuleFromMarkdown(
171
+ name: string,
172
+ content: string,
173
+ filePath: string,
174
+ source: SourceMeta,
175
+ options?: {
176
+ ruleName?: string;
177
+ stripNamePattern?: RegExp;
178
+ },
179
+ ): Rule {
180
+ const { frontmatter, body } = parseFrontmatter(content, { source: filePath });
181
+ const { condition, scope } = parseRuleConditionAndScope(frontmatter as RuleFrontmatter);
182
+
183
+ let globs: string[] | undefined;
184
+ if (Array.isArray(frontmatter.globs)) {
185
+ globs = frontmatter.globs.filter((item): item is string => typeof item === "string");
186
+ } else if (typeof frontmatter.globs === "string") {
187
+ globs = [frontmatter.globs];
188
+ }
189
+
190
+ const resolvedName = options?.ruleName ?? name.replace(options?.stripNamePattern ?? /\.(md|mdc)$/, "");
191
+ return {
192
+ name: resolvedName,
193
+ path: filePath,
194
+ content: body,
195
+ globs,
196
+ alwaysApply: frontmatter.alwaysApply === true,
197
+ description: typeof frontmatter.description === "string" ? frontmatter.description : undefined,
198
+ condition,
199
+ scope,
200
+ _source: source,
201
+ };
202
+ }
203
+
161
204
  /**
162
205
  * Parse model field into a prioritized list.
163
206
  */
@@ -29,6 +29,7 @@ import "./agents";
29
29
  import "./codex";
30
30
  import "./cursor";
31
31
  import "./gemini";
32
+ import "./opencode";
32
33
  import "./github";
33
34
  import "./mcp-json";
34
35
  import "./ssh";
@@ -0,0 +1,394 @@
1
+ /**
2
+ * OpenCode Discovery Provider
3
+ *
4
+ * Loads configuration from OpenCode's config directories:
5
+ * - User: ~/.config/opencode/
6
+ * - Project: .opencode/ (cwd) and opencode.json (project root)
7
+ *
8
+ * Capabilities:
9
+ * - context-files: AGENTS.md (user-level only at ~/.config/opencode/AGENTS.md)
10
+ * - mcps: From opencode.json "mcp" key
11
+ * - settings: From opencode.json
12
+ * - skills: From skills/ subdirectories
13
+ * - slash-commands: From commands/ subdirectories
14
+ * - extension-modules: From plugins/ subdirectories
15
+ *
16
+ * Priority: 55 (tool-specific provider)
17
+ */
18
+ import * as path from "node:path";
19
+ import { logger } from "@oh-my-pi/pi-utils";
20
+ import { registerProvider } from "../capability";
21
+ import { type ContextFile, contextFileCapability } from "../capability/context-file";
22
+ import { type ExtensionModule, extensionModuleCapability } from "../capability/extension-module";
23
+ import { readFile } from "../capability/fs";
24
+ import { type MCPServer, mcpCapability } from "../capability/mcp";
25
+ import { type Settings, settingsCapability } from "../capability/settings";
26
+ import { type Skill, skillCapability } from "../capability/skill";
27
+ import { type SlashCommand, slashCommandCapability } from "../capability/slash-command";
28
+ import type { LoadContext, LoadResult, SourceMeta } from "../capability/types";
29
+ import { parseFrontmatter } from "../utils/frontmatter";
30
+ import {
31
+ createSourceMeta,
32
+ discoverExtensionModulePaths,
33
+ expandEnvVarsDeep,
34
+ getExtensionNameFromPath,
35
+ getProjectPath,
36
+ getUserPath,
37
+ loadFilesFromDir,
38
+ loadSkillsFromDir,
39
+ parseJSON,
40
+ } from "./helpers";
41
+
42
+ const PROVIDER_ID = "opencode";
43
+ const DISPLAY_NAME = "OpenCode";
44
+ const PRIORITY = 55;
45
+
46
+ // =============================================================================
47
+ // JSON Config Loading
48
+ // =============================================================================
49
+
50
+ async function loadJsonConfig(configPath: string): Promise<Record<string, unknown> | null> {
51
+ const content = await readFile(configPath);
52
+ if (!content) return null;
53
+
54
+ const parsed = parseJSON<Record<string, unknown>>(content);
55
+ if (!parsed) {
56
+ logger.warn("Failed to parse OpenCode JSON config", { path: configPath });
57
+ return null;
58
+ }
59
+ return parsed;
60
+ }
61
+
62
+ // =============================================================================
63
+ // Context Files (AGENTS.md)
64
+ // =============================================================================
65
+
66
+ async function loadContextFiles(ctx: LoadContext): Promise<LoadResult<ContextFile>> {
67
+ const items: ContextFile[] = [];
68
+ const warnings: string[] = [];
69
+
70
+ // User-level only: ~/.config/opencode/AGENTS.md
71
+ const userAgentsMd = getUserPath(ctx, "opencode", "AGENTS.md");
72
+ if (userAgentsMd) {
73
+ const content = await readFile(userAgentsMd);
74
+ if (content) {
75
+ items.push({
76
+ path: userAgentsMd,
77
+ content,
78
+ level: "user",
79
+ _source: createSourceMeta(PROVIDER_ID, userAgentsMd, "user"),
80
+ });
81
+ }
82
+ }
83
+
84
+ return { items, warnings };
85
+ }
86
+
87
+ // =============================================================================
88
+ // MCP Servers (opencode.json → mcp)
89
+ // =============================================================================
90
+
91
+ /** OpenCode MCP server config (from opencode.json "mcp" key) */
92
+ interface OpenCodeMCPConfig {
93
+ type?: "local" | "remote";
94
+ command?: string;
95
+ args?: string[];
96
+ env?: Record<string, string>;
97
+ url?: string;
98
+ headers?: Record<string, string>;
99
+ enabled?: boolean;
100
+ timeout?: number;
101
+ }
102
+
103
+ async function loadMCPServers(ctx: LoadContext): Promise<LoadResult<MCPServer>> {
104
+ const items: MCPServer[] = [];
105
+ const warnings: string[] = [];
106
+
107
+ // User-level: ~/.config/opencode/opencode.json
108
+ const userConfigPath = getUserPath(ctx, "opencode", "opencode.json");
109
+ if (userConfigPath) {
110
+ const config = await loadJsonConfig(userConfigPath);
111
+ if (config) {
112
+ const result = extractMCPServers(config, userConfigPath, "user");
113
+ items.push(...result.items);
114
+ if (result.warnings) warnings.push(...result.warnings);
115
+ }
116
+ }
117
+
118
+ // Project-level: opencode.json in project root
119
+ const projectConfigPath = path.join(ctx.cwd, "opencode.json");
120
+ const projectConfig = await loadJsonConfig(projectConfigPath);
121
+ if (projectConfig) {
122
+ const result = extractMCPServers(projectConfig, projectConfigPath, "project");
123
+ items.push(...result.items);
124
+ if (result.warnings) warnings.push(...result.warnings);
125
+ }
126
+
127
+ return { items, warnings };
128
+ }
129
+
130
+ function extractMCPServers(
131
+ config: Record<string, unknown>,
132
+ configPath: string,
133
+ level: "user" | "project",
134
+ ): LoadResult<MCPServer> {
135
+ const items: MCPServer[] = [];
136
+ const warnings: string[] = [];
137
+
138
+ if (!config.mcp || typeof config.mcp !== "object") {
139
+ return { items, warnings };
140
+ }
141
+
142
+ const servers = expandEnvVarsDeep(config.mcp as Record<string, unknown>);
143
+
144
+ for (const [name, raw] of Object.entries(servers)) {
145
+ if (!raw || typeof raw !== "object") {
146
+ warnings.push(`Invalid MCP config for "${name}" in ${configPath}`);
147
+ continue;
148
+ }
149
+
150
+ const serverConfig = raw as OpenCodeMCPConfig;
151
+
152
+ // Determine transport from OpenCode's "type" field
153
+ let transport: "stdio" | "sse" | "http" | undefined;
154
+ if (serverConfig.type === "local") {
155
+ transport = "stdio";
156
+ } else if (serverConfig.type === "remote") {
157
+ transport = "http";
158
+ } else if (serverConfig.url) {
159
+ transport = "http";
160
+ } else if (serverConfig.command) {
161
+ transport = "stdio";
162
+ }
163
+
164
+ items.push({
165
+ name,
166
+ command: serverConfig.command,
167
+ args: Array.isArray(serverConfig.args) ? (serverConfig.args as string[]) : undefined,
168
+ env: serverConfig.env && typeof serverConfig.env === "object" ? serverConfig.env : undefined,
169
+ url: typeof serverConfig.url === "string" ? serverConfig.url : undefined,
170
+ headers: serverConfig.headers && typeof serverConfig.headers === "object" ? serverConfig.headers : undefined,
171
+ enabled: serverConfig.enabled,
172
+ timeout: typeof serverConfig.timeout === "number" ? serverConfig.timeout : undefined,
173
+ transport,
174
+ _source: createSourceMeta(PROVIDER_ID, configPath, level),
175
+ });
176
+ }
177
+
178
+ return { items, warnings };
179
+ }
180
+
181
+ // =============================================================================
182
+ // Skills (skills/)
183
+ // =============================================================================
184
+
185
+ async function loadSkills(ctx: LoadContext): Promise<LoadResult<Skill>> {
186
+ const userSkillsDir = getUserPath(ctx, "opencode", "skills");
187
+ const projectSkillsDir = getProjectPath(ctx, "opencode", "skills");
188
+
189
+ const promises: Promise<LoadResult<Skill>>[] = [];
190
+
191
+ if (userSkillsDir) {
192
+ promises.push(
193
+ loadSkillsFromDir(ctx, {
194
+ dir: userSkillsDir,
195
+ providerId: PROVIDER_ID,
196
+ level: "user",
197
+ }),
198
+ );
199
+ }
200
+
201
+ if (projectSkillsDir) {
202
+ promises.push(
203
+ loadSkillsFromDir(ctx, {
204
+ dir: projectSkillsDir,
205
+ providerId: PROVIDER_ID,
206
+ level: "project",
207
+ }),
208
+ );
209
+ }
210
+
211
+ const results = await Promise.all(promises);
212
+ const items = results.flatMap(r => r.items);
213
+ const warnings = results.flatMap(r => r.warnings || []);
214
+
215
+ return { items, warnings };
216
+ }
217
+
218
+ // =============================================================================
219
+ // Extension Modules (plugins/)
220
+ // =============================================================================
221
+
222
+ async function loadExtensionModules(ctx: LoadContext): Promise<LoadResult<ExtensionModule>> {
223
+ const userPluginsDir = getUserPath(ctx, "opencode", "plugins");
224
+ const projectPluginsDir = getProjectPath(ctx, "opencode", "plugins");
225
+
226
+ const [userPaths, projectPaths] = await Promise.all([
227
+ userPluginsDir ? discoverExtensionModulePaths(ctx, userPluginsDir) : Promise.resolve([]),
228
+ projectPluginsDir ? discoverExtensionModulePaths(ctx, projectPluginsDir) : Promise.resolve([]),
229
+ ]);
230
+
231
+ const items: ExtensionModule[] = [
232
+ ...userPaths.map(extPath => ({
233
+ name: getExtensionNameFromPath(extPath),
234
+ path: extPath,
235
+ level: "user" as const,
236
+ _source: createSourceMeta(PROVIDER_ID, extPath, "user"),
237
+ })),
238
+ ...projectPaths.map(extPath => ({
239
+ name: getExtensionNameFromPath(extPath),
240
+ path: extPath,
241
+ level: "project" as const,
242
+ _source: createSourceMeta(PROVIDER_ID, extPath, "project"),
243
+ })),
244
+ ];
245
+
246
+ return { items, warnings: [] };
247
+ }
248
+
249
+ // =============================================================================
250
+ // Slash Commands (commands/)
251
+ // =============================================================================
252
+
253
+ async function loadSlashCommands(ctx: LoadContext): Promise<LoadResult<SlashCommand>> {
254
+ const userCommandsDir = getUserPath(ctx, "opencode", "commands");
255
+ const projectCommandsDir = getProjectPath(ctx, "opencode", "commands");
256
+
257
+ const transformCommand =
258
+ (level: "user" | "project") => (name: string, content: string, filePath: string, source: SourceMeta) => {
259
+ const { frontmatter, body } = parseFrontmatter(content, { source: filePath });
260
+ const commandName = frontmatter.name || name.replace(/\.md$/, "");
261
+ return {
262
+ name: String(commandName),
263
+ path: filePath,
264
+ content: body,
265
+ level,
266
+ _source: source,
267
+ };
268
+ };
269
+
270
+ const promises: Promise<LoadResult<SlashCommand>>[] = [];
271
+
272
+ if (userCommandsDir) {
273
+ promises.push(
274
+ loadFilesFromDir(ctx, userCommandsDir, PROVIDER_ID, "user", {
275
+ extensions: ["md"],
276
+ transform: transformCommand("user"),
277
+ }),
278
+ );
279
+ }
280
+
281
+ if (projectCommandsDir) {
282
+ promises.push(
283
+ loadFilesFromDir(ctx, projectCommandsDir, PROVIDER_ID, "project", {
284
+ extensions: ["md"],
285
+ transform: transformCommand("project"),
286
+ }),
287
+ );
288
+ }
289
+
290
+ const results = await Promise.all(promises);
291
+ const items = results.flatMap(r => r.items);
292
+ const warnings = results.flatMap(r => r.warnings || []);
293
+
294
+ return { items, warnings };
295
+ }
296
+
297
+ // =============================================================================
298
+ // Settings (opencode.json)
299
+ // =============================================================================
300
+
301
+ async function loadSettings(ctx: LoadContext): Promise<LoadResult<Settings>> {
302
+ const items: Settings[] = [];
303
+ const warnings: string[] = [];
304
+
305
+ // User-level: ~/.config/opencode/opencode.json
306
+ const userConfigPath = getUserPath(ctx, "opencode", "opencode.json");
307
+ if (userConfigPath) {
308
+ const content = await readFile(userConfigPath);
309
+ if (content) {
310
+ const parsed = parseJSON<Record<string, unknown>>(content);
311
+ if (parsed) {
312
+ items.push({
313
+ path: userConfigPath,
314
+ data: parsed,
315
+ level: "user",
316
+ _source: createSourceMeta(PROVIDER_ID, userConfigPath, "user"),
317
+ });
318
+ } else {
319
+ warnings.push(`Invalid JSON in ${userConfigPath}`);
320
+ }
321
+ }
322
+ }
323
+
324
+ // Project-level: opencode.json in project root
325
+ const projectConfigPath = path.join(ctx.cwd, "opencode.json");
326
+ const content = await readFile(projectConfigPath);
327
+ if (content) {
328
+ const parsed = parseJSON<Record<string, unknown>>(content);
329
+ if (parsed) {
330
+ items.push({
331
+ path: projectConfigPath,
332
+ data: parsed,
333
+ level: "project",
334
+ _source: createSourceMeta(PROVIDER_ID, projectConfigPath, "project"),
335
+ });
336
+ } else {
337
+ warnings.push(`Invalid JSON in ${projectConfigPath}`);
338
+ }
339
+ }
340
+
341
+ return { items, warnings };
342
+ }
343
+
344
+ // =============================================================================
345
+ // Provider Registration
346
+ // =============================================================================
347
+
348
+ registerProvider(contextFileCapability.id, {
349
+ id: PROVIDER_ID,
350
+ displayName: DISPLAY_NAME,
351
+ description: "Load AGENTS.md from ~/.config/opencode/",
352
+ priority: PRIORITY,
353
+ load: loadContextFiles,
354
+ });
355
+
356
+ registerProvider(mcpCapability.id, {
357
+ id: PROVIDER_ID,
358
+ displayName: DISPLAY_NAME,
359
+ description: "Load MCP servers from opencode.json mcp key",
360
+ priority: PRIORITY,
361
+ load: loadMCPServers,
362
+ });
363
+
364
+ registerProvider(skillCapability.id, {
365
+ id: PROVIDER_ID,
366
+ displayName: DISPLAY_NAME,
367
+ description: "Load skills from ~/.config/opencode/skills/ and .opencode/skills/",
368
+ priority: PRIORITY,
369
+ load: loadSkills,
370
+ });
371
+
372
+ registerProvider(extensionModuleCapability.id, {
373
+ id: PROVIDER_ID,
374
+ displayName: DISPLAY_NAME,
375
+ description: "Load extension modules from ~/.config/opencode/plugins/ and .opencode/plugins/",
376
+ priority: PRIORITY,
377
+ load: loadExtensionModules,
378
+ });
379
+
380
+ registerProvider(slashCommandCapability.id, {
381
+ id: PROVIDER_ID,
382
+ displayName: DISPLAY_NAME,
383
+ description: "Load slash commands from ~/.config/opencode/commands/ and .opencode/commands/",
384
+ priority: PRIORITY,
385
+ load: loadSlashCommands,
386
+ });
387
+
388
+ registerProvider(settingsCapability.id, {
389
+ id: PROVIDER_ID,
390
+ displayName: DISPLAY_NAME,
391
+ description: "Load settings from opencode.json",
392
+ priority: PRIORITY,
393
+ load: loadSettings,
394
+ });
@@ -15,8 +15,8 @@ import { readFile } from "../capability/fs";
15
15
  import { type MCPServer, mcpCapability } from "../capability/mcp";
16
16
  import { type Rule, ruleCapability } from "../capability/rule";
17
17
  import type { LoadContext, LoadResult } from "../capability/types";
18
- import { parseFrontmatter } from "../utils/frontmatter";
19
18
  import {
19
+ buildRuleFromMarkdown,
20
20
  createSourceMeta,
21
21
  expandEnvVarsDeep,
22
22
  getProjectPath,
@@ -104,26 +104,8 @@ async function loadRules(ctx: LoadContext): Promise<LoadResult<Rule>> {
104
104
  if (userPath) {
105
105
  const content = await readFile(userPath);
106
106
  if (content) {
107
- const { frontmatter, body } = parseFrontmatter(content, { source: userPath });
108
-
109
- // Validate and normalize globs
110
- let globs: string[] | undefined;
111
- if (Array.isArray(frontmatter.globs)) {
112
- globs = frontmatter.globs.filter((g): g is string => typeof g === "string");
113
- } else if (typeof frontmatter.globs === "string") {
114
- globs = [frontmatter.globs];
115
- }
116
-
117
- items.push({
118
- name: "global_rules",
119
- path: userPath,
120
- content: body,
121
- globs,
122
- alwaysApply: frontmatter.alwaysApply as boolean | undefined,
123
- description: frontmatter.description as string | undefined,
124
- ttsrTrigger: typeof frontmatter.ttsr_trigger === "string" ? frontmatter.ttsr_trigger : undefined,
125
- _source: createSourceMeta(PROVIDER_ID, userPath, "user"),
126
- });
107
+ const source = createSourceMeta(PROVIDER_ID, userPath, "user");
108
+ items.push(buildRuleFromMarkdown("global_rules.md", content, userPath, source, { ruleName: "global_rules" }));
127
109
  }
128
110
  }
129
111
 
@@ -132,29 +114,8 @@ async function loadRules(ctx: LoadContext): Promise<LoadResult<Rule>> {
132
114
  if (projectRulesDir) {
133
115
  const result = await loadFilesFromDir<Rule>(ctx, projectRulesDir, PROVIDER_ID, "project", {
134
116
  extensions: ["md"],
135
- transform: (name, content, path, source) => {
136
- const { frontmatter, body } = parseFrontmatter(content, { source: path });
137
- const ruleName = name.replace(/\.md$/, "");
138
-
139
- // Validate and normalize globs
140
- let globs: string[] | undefined;
141
- if (Array.isArray(frontmatter.globs)) {
142
- globs = frontmatter.globs.filter((g): g is string => typeof g === "string");
143
- } else if (typeof frontmatter.globs === "string") {
144
- globs = [frontmatter.globs];
145
- }
146
-
147
- return {
148
- name: ruleName,
149
- path,
150
- content: body,
151
- globs,
152
- alwaysApply: frontmatter.alwaysApply as boolean | undefined,
153
- description: frontmatter.description as string | undefined,
154
- ttsrTrigger: typeof frontmatter.ttsr_trigger === "string" ? frontmatter.ttsr_trigger : undefined,
155
- _source: source,
156
- };
157
- },
117
+ transform: (name, content, path, source) =>
118
+ buildRuleFromMarkdown(name, content, path, source, { stripNamePattern: /\.md$/ }),
158
119
  });
159
120
  items.push(...result.items);
160
121
  if (result.warnings) warnings.push(...result.warnings);