@oh-my-pi/pi-coding-agent 12.9.0 → 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.
@@ -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";
@@ -163,6 +164,43 @@ export function parseArrayOrCSV(value: unknown): string[] | undefined {
163
164
  return undefined;
164
165
  }
165
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
+
166
204
  /**
167
205
  * Parse model field into a prioritized list.
168
206
  */
@@ -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);