bitcompass 0.3.7 → 0.3.8

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.
@@ -4,6 +4,7 @@ import chalk from 'chalk';
4
4
  import { loadCredentials } from '../auth/config.js';
5
5
  import { searchRules, fetchRules, getRuleById, insertRule } from '../api/client.js';
6
6
  import { pullRuleToFile } from '../lib/rule-file-ops.js';
7
+ import { parseRuleMdcContent } from '../lib/mdc-format.js';
7
8
  import { formatList, shouldUseTable } from '../lib/list-format.js';
8
9
  export const runRulesSearch = async (query, options) => {
9
10
  if (!loadCredentials()) {
@@ -108,9 +109,23 @@ export const runRulesPush = async (file) => {
108
109
  payload = JSON.parse(raw);
109
110
  }
110
111
  catch {
111
- const lines = raw.split('\n');
112
- const title = lines[0].replace(/^#\s*/, '') || 'Untitled';
113
- payload = { kind: 'rule', title, description: '', body: raw };
112
+ const parsed = parseRuleMdcContent(raw);
113
+ if (parsed) {
114
+ const titleFromBody = parsed.body.split('\n')[0]?.replace(/^#\s*/, '').trim() || 'Untitled';
115
+ payload = {
116
+ kind: 'rule',
117
+ title: titleFromBody,
118
+ description: parsed.description,
119
+ body: parsed.body,
120
+ globs: parsed.globs ?? undefined,
121
+ always_apply: parsed.alwaysApply,
122
+ };
123
+ }
124
+ else {
125
+ const lines = raw.split('\n');
126
+ const title = lines[0].replace(/^#\s*/, '') || 'Untitled';
127
+ payload = { kind: 'rule', title, description: '', body: raw };
128
+ }
114
129
  }
115
130
  }
116
131
  else {
@@ -0,0 +1,16 @@
1
+ import type { Rule } from '../types.js';
2
+ /**
3
+ * Builds Cursor .mdc content for a rule: YAML frontmatter (description, globs, alwaysApply) then body.
4
+ */
5
+ export declare const buildRuleMdcContent: (rule: Rule) => string;
6
+ export interface ParsedMdcFrontmatter {
7
+ description: string;
8
+ globs?: string;
9
+ alwaysApply: boolean;
10
+ body: string;
11
+ }
12
+ /**
13
+ * Parses .mdc content: frontmatter (description, globs, alwaysApply) and body.
14
+ * Returns null if the file does not start with --- (not frontmatter format).
15
+ */
16
+ export declare const parseRuleMdcContent: (raw: string) => ParsedMdcFrontmatter | null;
@@ -0,0 +1,66 @@
1
+ const FRONTMATTER_DELIM = '---';
2
+ /**
3
+ * Builds Cursor .mdc content for a rule: YAML frontmatter (description, globs, alwaysApply) then body.
4
+ */
5
+ export const buildRuleMdcContent = (rule) => {
6
+ const lines = [FRONTMATTER_DELIM];
7
+ lines.push(`description: ${escapeYamlValue(rule.description ?? '')}`);
8
+ if (rule.globs != null && String(rule.globs).trim() !== '') {
9
+ lines.push(`globs: ${escapeYamlValue(String(rule.globs).trim())}`);
10
+ }
11
+ lines.push(`alwaysApply: ${rule.always_apply === true}`);
12
+ lines.push(FRONTMATTER_DELIM);
13
+ lines.push('');
14
+ lines.push(rule.body.trimEnd());
15
+ if (!rule.body.endsWith('\n')) {
16
+ lines.push('');
17
+ }
18
+ return lines.join('\n');
19
+ };
20
+ const escapeYamlValue = (s) => {
21
+ if (/^[a-z0-9-]+$/i.test(s) && !s.includes(':'))
22
+ return s;
23
+ return `"${s.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n')}"`;
24
+ };
25
+ /**
26
+ * Parses .mdc content: frontmatter (description, globs, alwaysApply) and body.
27
+ * Returns null if the file does not start with --- (not frontmatter format).
28
+ */
29
+ export const parseRuleMdcContent = (raw) => {
30
+ const trimmed = raw.trimStart();
31
+ if (!trimmed.startsWith(FRONTMATTER_DELIM)) {
32
+ return null;
33
+ }
34
+ const rest = trimmed.slice(FRONTMATTER_DELIM.length);
35
+ const endIdx = rest.indexOf('\n' + FRONTMATTER_DELIM);
36
+ if (endIdx === -1) {
37
+ return null;
38
+ }
39
+ const frontmatterBlock = rest.slice(0, endIdx).trim();
40
+ const body = rest.slice(endIdx + FRONTMATTER_DELIM.length + 1).trimStart();
41
+ let description = '';
42
+ let globs;
43
+ let alwaysApply = false;
44
+ for (const line of frontmatterBlock.split('\n')) {
45
+ const colonIdx = line.indexOf(':');
46
+ if (colonIdx === -1)
47
+ continue;
48
+ const key = line.slice(0, colonIdx).trim();
49
+ let value = line.slice(colonIdx + 1).trim();
50
+ if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
51
+ value = value.slice(1, -1).replace(/\\n/g, '\n').replace(/\\"/g, '"').replace(/\\\\/g, '\\');
52
+ }
53
+ switch (key) {
54
+ case 'description':
55
+ description = value;
56
+ break;
57
+ case 'globs':
58
+ globs = value;
59
+ break;
60
+ case 'alwaysApply':
61
+ alwaysApply = value === 'true' || value === '1';
62
+ break;
63
+ }
64
+ }
65
+ return { description, globs, alwaysApply, body };
66
+ };
@@ -4,11 +4,11 @@ import type { Rule } from '../types.js';
4
4
  */
5
5
  export declare const getCacheDir: () => string;
6
6
  /**
7
- * Gets the cached file path for a rule by ID
7
+ * Gets the cached file path for a rule by ID (rules use .mdc, others use .md)
8
8
  */
9
9
  export declare const getCachedRulePath: (rule: Rule) => string;
10
10
  /**
11
11
  * Ensures a rule is cached. Downloads and caches it if not present or outdated.
12
- * Returns the path to the cached file.
12
+ * Returns the path to the cached file. Rules are stored as .mdc with Cursor frontmatter.
13
13
  */
14
14
  export declare const ensureRuleCached: (id: string) => Promise<string>;
@@ -2,7 +2,8 @@ import { existsSync, mkdirSync, writeFileSync, statSync } from 'fs';
2
2
  import { join } from 'path';
3
3
  import { getConfigDir } from '../auth/config.js';
4
4
  import { getRuleById } from '../api/client.js';
5
- import { ruleFilename, solutionFilename } from './slug.js';
5
+ import { ruleFilename, solutionFilename, skillFilename, commandFilename } from './slug.js';
6
+ import { buildRuleMdcContent } from './mdc-format.js';
6
7
  /**
7
8
  * Gets the cache directory for rules (~/.bitcompass/cache/rules/)
8
9
  */
@@ -14,18 +15,22 @@ export const getCacheDir = () => {
14
15
  return cacheDir;
15
16
  };
16
17
  /**
17
- * Gets the cached file path for a rule by ID
18
+ * Gets the cached file path for a rule by ID (rules use .mdc, others use .md)
18
19
  */
19
20
  export const getCachedRulePath = (rule) => {
20
21
  const cacheDir = getCacheDir();
21
22
  const filename = rule.kind === 'solution'
22
23
  ? solutionFilename(rule.title, rule.id)
23
- : ruleFilename(rule.title, rule.id);
24
+ : rule.kind === 'skill'
25
+ ? skillFilename(rule.title, rule.id)
26
+ : rule.kind === 'command'
27
+ ? commandFilename(rule.title, rule.id)
28
+ : ruleFilename(rule.title, rule.id);
24
29
  return join(cacheDir, `${rule.id}-${filename}`);
25
30
  };
26
31
  /**
27
32
  * Ensures a rule is cached. Downloads and caches it if not present or outdated.
28
- * Returns the path to the cached file.
33
+ * Returns the path to the cached file. Rules are stored as .mdc with Cursor frontmatter.
29
34
  */
30
35
  export const ensureRuleCached = async (id) => {
31
36
  const rule = await getRuleById(id);
@@ -35,9 +40,11 @@ export const ensureRuleCached = async (id) => {
35
40
  const cachedPath = getCachedRulePath(rule);
36
41
  const needsUpdate = !existsSync(cachedPath) || isCacheOutdated(cachedPath, rule);
37
42
  if (needsUpdate) {
38
- const content = rule.kind === 'solution'
39
- ? `# ${rule.title}\n\n${rule.description}\n\n## Solution\n\n${rule.body}\n`
40
- : `# ${rule.title}\n\n${rule.description}\n\n${rule.body}\n`;
43
+ const content = rule.kind === 'rule'
44
+ ? buildRuleMdcContent(rule)
45
+ : rule.kind === 'solution'
46
+ ? `# ${rule.title}\n\n${rule.description}\n\n## Solution\n\n${rule.body}\n`
47
+ : `# ${rule.title}\n\n${rule.description}\n\n${rule.body}\n`;
41
48
  writeFileSync(cachedPath, content, 'utf-8');
42
49
  }
43
50
  return cachedPath;
@@ -5,6 +5,7 @@ import { getRuleById } from '../api/client.js';
5
5
  import { getProjectConfig } from '../auth/project-config.js';
6
6
  import { ruleFilename, solutionFilename, skillFilename, commandFilename } from './slug.js';
7
7
  import { ensureRuleCached } from './rule-cache.js';
8
+ import { buildRuleMdcContent } from './mdc-format.js';
8
9
  /**
9
10
  * Pulls a rule or solution to a file using symbolic links (like Bun init).
10
11
  * Returns the file path where it was written/linked.
@@ -79,23 +80,12 @@ export const pullRuleToFile = async (id, options = {}) => {
79
80
  }
80
81
  }
81
82
  else {
82
- // Fallback: copy file content (for compatibility or when symlinks aren't desired)
83
- let content;
84
- switch (rule.kind) {
85
- case 'solution':
86
- content = `# ${rule.title}\n\n${rule.description}\n\n## Solution\n\n${rule.body}\n`;
87
- break;
88
- case 'skill':
89
- content = `# ${rule.title}\n\n${rule.description}\n\n${rule.body}\n`;
90
- break;
91
- case 'command':
92
- content = `# ${rule.title}\n\n${rule.description}\n\n${rule.body}\n`;
93
- break;
94
- case 'rule':
95
- default:
96
- content = `# ${rule.title}\n\n${rule.description}\n\n${rule.body}\n`;
97
- break;
98
- }
83
+ // Fallback: copy file content (rules as .mdc with frontmatter, others as .md)
84
+ const content = rule.kind === 'rule'
85
+ ? buildRuleMdcContent(rule)
86
+ : rule.kind === 'solution'
87
+ ? `# ${rule.title}\n\n${rule.description}\n\n## Solution\n\n${rule.body}\n`
88
+ : `# ${rule.title}\n\n${rule.description}\n\n${rule.body}\n`;
99
89
  writeFileSync(filename, content);
100
90
  }
101
91
  return filename;
@@ -4,7 +4,7 @@
4
4
  */
5
5
  export declare const titleToSlug: (title: string) => string;
6
6
  /**
7
- * Returns the rule filename (e.g. rule-strava-api-authentication-flow.md).
7
+ * Returns the rule filename in Cursor .mdc format (e.g. rule-strava-api-authentication-flow.mdc).
8
8
  * Falls back to id if slug is empty.
9
9
  */
10
10
  export declare const ruleFilename: (title: string, id: string) => string;
package/dist/lib/slug.js CHANGED
@@ -14,13 +14,13 @@ export const titleToSlug = (title) => {
14
14
  .replace(/^-|-$/g, '');
15
15
  };
16
16
  /**
17
- * Returns the rule filename (e.g. rule-strava-api-authentication-flow.md).
17
+ * Returns the rule filename in Cursor .mdc format (e.g. rule-strava-api-authentication-flow.mdc).
18
18
  * Falls back to id if slug is empty.
19
19
  */
20
20
  export const ruleFilename = (title, id) => {
21
21
  const slug = titleToSlug(title);
22
22
  const base = slug ? `rule-${slug}` : `rule-${id}`;
23
- return `${base}.md`;
23
+ return `${base}.mdc`;
24
24
  };
25
25
  /**
26
26
  * Returns the solution filename (e.g. solution-strava-api-authentication-flow.md).
@@ -139,7 +139,7 @@ function createStdioServer() {
139
139
  },
140
140
  {
141
141
  name: 'update-rule',
142
- description: 'Use when the user wants to edit an existing rule or solution they own. Pass id and any fields to update (title, description, body, context, examples, technologies). Returns updated metadata. Requires authentication.',
142
+ description: 'Use when the user wants to edit an existing rule or solution they own. Pass id and any fields to update (title, description, body, context, examples, technologies, globs, always_apply). Returns updated metadata. Requires authentication.',
143
143
  inputSchema: {
144
144
  type: 'object',
145
145
  properties: {
@@ -150,6 +150,8 @@ function createStdioServer() {
150
150
  context: { type: 'string', description: 'Updated context' },
151
151
  examples: { type: 'array', items: { type: 'string' }, description: 'Updated examples array' },
152
152
  technologies: { type: 'array', items: { type: 'string' }, description: 'Updated technologies array' },
153
+ globs: { type: 'string', description: 'Glob patterns for when rule applies (e.g. "*.ts, *.tsx")' },
154
+ always_apply: { type: 'boolean', description: 'If true, Cursor applies this rule globally' },
153
155
  },
154
156
  required: ['id'],
155
157
  },
@@ -399,6 +401,8 @@ function createStdioServer() {
399
401
  context: rule.context ?? null,
400
402
  examples: rule.examples ?? [],
401
403
  technologies: rule.technologies ?? [],
404
+ globs: rule.globs ?? null,
405
+ always_apply: rule.always_apply ?? false,
402
406
  author: rule.author_display_name ?? null,
403
407
  created_at: rule.created_at,
404
408
  updated_at: rule.updated_at,
@@ -459,6 +463,10 @@ function createStdioServer() {
459
463
  updates.examples = args.examples;
460
464
  if (Array.isArray(args.technologies))
461
465
  updates.technologies = args.technologies;
466
+ if (args.globs !== undefined)
467
+ updates.globs = args.globs || null;
468
+ if (args.always_apply !== undefined)
469
+ updates.always_apply = Boolean(args.always_apply);
462
470
  try {
463
471
  const updated = await updateRule(id, updates);
464
472
  return {
package/dist/types.d.ts CHANGED
@@ -11,6 +11,10 @@ export interface Rule {
11
11
  user_id: string;
12
12
  author_display_name?: string | null;
13
13
  version?: string | null;
14
+ /** Optional glob patterns for when the rule applies (e.g. "*.ts, *.tsx"). Used in .mdc frontmatter. */
15
+ globs?: string | null;
16
+ /** If true, Cursor applies this rule globally. Default false. Used in .mdc frontmatter. */
17
+ always_apply?: boolean;
14
18
  created_at: string;
15
19
  updated_at: string;
16
20
  }
@@ -23,6 +27,8 @@ export interface RuleInsert {
23
27
  examples?: string[];
24
28
  technologies?: string[];
25
29
  version?: string;
30
+ globs?: string | null;
31
+ always_apply?: boolean;
26
32
  }
27
33
  export interface StoredCredentials {
28
34
  access_token: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bitcompass",
3
- "version": "0.3.7",
3
+ "version": "0.3.8",
4
4
  "description": "BitCompass CLI - rules, solutions, and MCP server",
5
5
  "type": "module",
6
6
  "bin": {