bitcompass 0.3.9 → 0.4.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.
@@ -2,9 +2,10 @@ import inquirer from 'inquirer';
2
2
  import ora from 'ora';
3
3
  import chalk from 'chalk';
4
4
  import { loadCredentials } from '../auth/config.js';
5
- import { searchRules, fetchRules, getRuleById, insertRule } from '../api/client.js';
5
+ import { searchRules, fetchRules, getRuleById } from '../api/client.js';
6
6
  import { pullRuleToFile } from '../lib/rule-file-ops.js';
7
7
  import { formatList, shouldUseTable } from '../lib/list-format.js';
8
+ import { runSharePush } from './share.js';
8
9
  export const runSolutionsSearch = async (query, options) => {
9
10
  if (!loadCredentials()) {
10
11
  console.error(chalk.red('Not logged in. Run bitcompass login.'));
@@ -95,35 +96,6 @@ export const runSolutionsPull = async (id, options) => {
95
96
  process.exit(message.includes('not found') ? 2 : 1);
96
97
  }
97
98
  };
98
- export const runSolutionsPush = async (file) => {
99
- if (!loadCredentials()) {
100
- console.error(chalk.red('Not logged in. Run bitcompass login.'));
101
- process.exit(1);
102
- }
103
- let payload;
104
- if (file) {
105
- const { readFileSync } = await import('fs');
106
- const raw = readFileSync(file, 'utf-8');
107
- try {
108
- payload = JSON.parse(raw);
109
- payload.kind = 'solution';
110
- }
111
- catch {
112
- const lines = raw.split('\n');
113
- const title = lines[0].replace(/^#\s*/, '') || 'Untitled';
114
- payload = { kind: 'solution', title, description: '', body: raw };
115
- }
116
- }
117
- else {
118
- const answers = await inquirer.prompt([
119
- { name: 'title', message: 'Problem title', type: 'input', default: 'Untitled' },
120
- { name: 'description', message: 'Description', type: 'input', default: '' },
121
- { name: 'body', message: 'Solution content', type: 'editor', default: '' },
122
- ]);
123
- payload = { kind: 'solution', title: answers.title, description: answers.description, body: answers.body };
124
- }
125
- const spinner = ora('Publishing solution…').start();
126
- const created = await insertRule(payload);
127
- spinner.succeed(chalk.green('Published solution ') + created.id);
128
- console.log(chalk.dim(created.title));
99
+ export const runSolutionsPush = async (file, options) => {
100
+ await runSharePush(file, { kind: 'solution', projectId: options?.projectId });
129
101
  };
@@ -1,4 +1,4 @@
1
- import { loadCredentials } from '../auth/config.js';
1
+ import { getCurrentUserEmail, loadCredentials } from '../auth/config.js';
2
2
  import chalk from 'chalk';
3
3
  export const runWhoami = () => {
4
4
  const creds = loadCredentials();
@@ -6,7 +6,7 @@ export const runWhoami = () => {
6
6
  console.error(chalk.red('Not logged in. Run bitcompass login.'));
7
7
  process.exit(1);
8
8
  }
9
- const email = creds.user?.email;
9
+ const email = getCurrentUserEmail();
10
10
  if (email) {
11
11
  console.log(email);
12
12
  }
package/dist/index.js CHANGED
@@ -5,17 +5,18 @@ import { Command } from 'commander';
5
5
  import { readFileSync } from 'fs';
6
6
  import { dirname, join } from 'path';
7
7
  import { fileURLToPath } from 'url';
8
+ import { runCommandsList, runCommandsPull, runCommandsPush, runCommandsSearch } from './commands/commands.js';
8
9
  import { runConfigGet, runConfigList, runConfigSet } from './commands/config-cmd.js';
10
+ import { runGlossary } from './commands/glossary.js';
9
11
  import { runInit } from './commands/init.js';
12
+ import { runLog, ValidationError } from './commands/log.js';
10
13
  import { runLogin } from './commands/login.js';
11
14
  import { runLogout } from './commands/logout.js';
12
15
  import { runMcpStart, runMcpStatus } from './commands/mcp.js';
13
- import { runLog, ValidationError } from './commands/log.js';
14
16
  import { runRulesList, runRulesPull, runRulesPush, runRulesSearch } from './commands/rules.js';
15
- import { runSolutionsList, runSolutionsPull, runSolutionsPush, runSolutionsSearch } from './commands/solutions.js';
17
+ import { runSharePush } from './commands/share.js';
16
18
  import { runSkillsList, runSkillsPull, runSkillsPush, runSkillsSearch } from './commands/skills.js';
17
- import { runCommandsList, runCommandsPull, runCommandsPush, runCommandsSearch } from './commands/commands.js';
18
- import { runGlossary } from './commands/glossary.js';
19
+ import { runSolutionsList, runSolutionsPull, runSolutionsPush, runSolutionsSearch } from './commands/solutions.js';
19
20
  import { runWhoami } from './commands/whoami.js';
20
21
  // Disable chalk colors when NO_COLOR is set or --no-color is passed (must run before any command)
21
22
  if (process.env.NO_COLOR !== undefined || process.argv.includes('--no-color')) {
@@ -48,6 +49,25 @@ program
48
49
  .command('glossary')
49
50
  .description('Show glossary (rules, solutions, skills, commands)')
50
51
  .action(runGlossary);
52
+ program
53
+ .command('share [file]')
54
+ .description('Share a rule, solution, skill, or command (prompts for type if not in file or --kind)')
55
+ .option('-k, --kind <kind>', 'Type: rule, solution, skill, or command (skips type prompt)')
56
+ .option('--project-id <uuid>', 'Scope to Compass project (UUID)')
57
+ .addHelpText('after', `
58
+ Examples:
59
+ bitcompass share
60
+ bitcompass share ./my-rule.mdc
61
+ bitcompass share ./doc.md --kind solution
62
+ `)
63
+ .action((file, opts) => {
64
+ const kind = opts?.kind;
65
+ if (opts?.kind && kind !== 'rule' && kind !== 'solution' && kind !== 'skill' && kind !== 'command') {
66
+ console.error(chalk.red('--kind must be one of: rule, solution, skill, command'));
67
+ process.exit(1);
68
+ }
69
+ return runSharePush(file, { kind, projectId: opts?.projectId }).catch(handleErr);
70
+ });
51
71
  program
52
72
  .command('init')
53
73
  .description('Configure project: editor/AI provider and output folder for rules/docs/commands')
@@ -93,7 +113,11 @@ rules
93
113
  .option('--copy', 'Copy file instead of creating symbolic link')
94
114
  .addHelpText('after', '\nExamples:\n bitcompass rules pull <id>\n bitcompass rules pull <id> --global\n bitcompass rules pull <id> --copy\n')
95
115
  .action((id, options) => runRulesPull(id, options).catch(handleErr));
96
- rules.command('push [file]').description('Push a rule (file or interactive)').action((file) => runRulesPush(file).catch(handleErr));
116
+ rules
117
+ .command('push [file]')
118
+ .description('Push a rule (file or interactive)')
119
+ .option('--project-id <uuid>', 'Scope to Compass project (UUID)')
120
+ .action((file, opts) => runRulesPush(file, { projectId: opts?.projectId }).catch(handleErr));
97
121
  // solutions
98
122
  const solutions = program.command('solutions').description('Manage solutions');
99
123
  solutions
@@ -114,7 +138,11 @@ solutions
114
138
  .option('--copy', 'Copy file instead of creating symbolic link')
115
139
  .addHelpText('after', '\nExamples:\n bitcompass solutions pull <id>\n bitcompass solutions pull <id> --global\n')
116
140
  .action((id, options) => runSolutionsPull(id, options).catch(handleErr));
117
- solutions.command('push [file]').description('Push a solution (file or interactive)').action((file) => runSolutionsPush(file).catch(handleErr));
141
+ solutions
142
+ .command('push [file]')
143
+ .description('Push a solution (file or interactive)')
144
+ .option('--project-id <uuid>', 'Scope to Compass project (UUID)')
145
+ .action((file, opts) => runSolutionsPush(file, { projectId: opts?.projectId }).catch(handleErr));
118
146
  // skills
119
147
  const skills = program.command('skills').description('Manage skills');
120
148
  skills
@@ -135,7 +163,11 @@ skills
135
163
  .option('--copy', 'Copy file instead of creating symbolic link')
136
164
  .addHelpText('after', '\nExamples:\n bitcompass skills pull <id>\n bitcompass skills pull <id> --global\n')
137
165
  .action((id, options) => runSkillsPull(id, options).catch(handleErr));
138
- skills.command('push [file]').description('Push a skill (file or interactive)').action((file) => runSkillsPush(file).catch(handleErr));
166
+ skills
167
+ .command('push [file]')
168
+ .description('Push a skill (file or interactive)')
169
+ .option('--project-id <uuid>', 'Scope to Compass project (UUID)')
170
+ .action((file, opts) => runSkillsPush(file, { projectId: opts?.projectId }).catch(handleErr));
139
171
  // commands
140
172
  const commands = program.command('commands').description('Manage commands');
141
173
  commands
@@ -156,7 +188,11 @@ commands
156
188
  .option('--copy', 'Copy file instead of creating symbolic link')
157
189
  .addHelpText('after', '\nExamples:\n bitcompass commands pull <id>\n bitcompass commands pull <id> --global\n')
158
190
  .action((id, options) => runCommandsPull(id, options).catch(handleErr));
159
- commands.command('push [file]').description('Push a command (file or interactive)').action((file) => runCommandsPush(file).catch(handleErr));
191
+ commands
192
+ .command('push [file]')
193
+ .description('Push a command (file or interactive)')
194
+ .option('--project-id <uuid>', 'Scope to Compass project (UUID)')
195
+ .action((file, opts) => runCommandsPush(file, { projectId: opts?.projectId }).catch(handleErr));
160
196
  // mcp
161
197
  const mcp = program.command('mcp').description('MCP server');
162
198
  mcp.command('start').description('Start MCP server (stdio)').action(() => runMcpStart().catch(handleErr));
@@ -0,0 +1,15 @@
1
+ type Rgb = [number, number, number];
2
+ /**
3
+ * Renders text with a gradient between two RGB colors.
4
+ */
5
+ export declare const gradient: (text: string, start: Rgb, end: Rgb) => string;
6
+ /**
7
+ * Glow effect: gradient that peaks in the middle (bright center, dimmer edges).
8
+ */
9
+ export declare const glow: (text: string, baseColor: Rgb) => string;
10
+ /**
11
+ * Prints the same version + "made by" banner as postinstall (for bitcompass init).
12
+ * Reads version from package.json next to dist/.
13
+ */
14
+ export declare const printBanner: () => Promise<void>;
15
+ export {};
@@ -0,0 +1,82 @@
1
+ import chalk from 'chalk';
2
+ import { readFileSync } from 'fs';
3
+ import { dirname, join } from 'path';
4
+ import { fileURLToPath } from 'url';
5
+ /**
6
+ * Renders text with a gradient between two RGB colors.
7
+ */
8
+ export const gradient = (text, start, end) => [...text]
9
+ .map((char, i) => {
10
+ const t = text.length > 1 ? i / (text.length - 1) : 1;
11
+ const [r1, g1, b1] = start;
12
+ const [r2, g2, b2] = end;
13
+ const r = Math.round(r1 + (r2 - r1) * t);
14
+ const g = Math.round(g1 + (g2 - g1) * t);
15
+ const b = Math.round(b1 + (b2 - b1) * t);
16
+ return chalk.rgb(r, g, b)(char);
17
+ })
18
+ .join('');
19
+ /**
20
+ * Glow effect: gradient that peaks in the middle (bright center, dimmer edges).
21
+ */
22
+ export const glow = (text, baseColor) => [...text]
23
+ .map((char, i) => {
24
+ const t = text.length > 1 ? i / (text.length - 1) : 1;
25
+ const peak = 1 - 4 * (t - 0.5) * (t - 0.5);
26
+ const mult = 0.4 + 0.6 * Math.max(0, peak);
27
+ const [r, g, b] = baseColor;
28
+ const R = Math.round(r * mult);
29
+ const G = Math.round(g * mult);
30
+ const B = Math.round(b * mult);
31
+ return chalk.rgb(R, G, B)(char);
32
+ })
33
+ .join('');
34
+ const cyan = [0, 212, 255];
35
+ const magenta = [255, 64, 200];
36
+ const glowColor = [255, 120, 255];
37
+ const PREFIX = 'made by ';
38
+ const AUTHOR = '@davide97g';
39
+ const GITHUB_URL = 'https://github.com/davide97g/';
40
+ /** OSC 8 hyperlink for supported terminals. */
41
+ const link = (url, text) => `\x1b]8;;${url}\x1b\\${text}\x1b]8;;\x1b\\`;
42
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
43
+ /**
44
+ * Prints the same version + "made by" banner as postinstall (for bitcompass init).
45
+ * Reads version from package.json next to dist/.
46
+ */
47
+ export const printBanner = async () => {
48
+ const __dirname = dirname(fileURLToPath(import.meta.url));
49
+ const pkgPath = join(__dirname, '..', '..', 'package.json');
50
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
51
+ const version = pkg.version ?? '0.0.0';
52
+ const out = process.stdout;
53
+ const isTty = out.isTTY;
54
+ const frames = 6;
55
+ const frameMs = 80;
56
+ out.write('\n');
57
+ out.write(gradient(`version ${version}`, cyan, magenta) + '\n');
58
+ if (!isTty) {
59
+ out.write(PREFIX + link(GITHUB_URL, glow(AUTHOR, glowColor)) + '\n');
60
+ out.write('\n');
61
+ return;
62
+ }
63
+ for (let f = 0; f < frames; f++) {
64
+ const t = f / (frames - 1);
65
+ const brightness = 0.3 + 0.7 * (1 - Math.cos(t * Math.PI));
66
+ const styledAuthor = [...AUTHOR]
67
+ .map((char, i) => {
68
+ const ti = AUTHOR.length > 1 ? i / (AUTHOR.length - 1) : 1;
69
+ const peak = 1 - 4 * (ti - 0.5) * (ti - 0.5);
70
+ const mult = (0.4 + 0.6 * Math.max(0, peak)) * brightness;
71
+ const [r, g, b] = glowColor;
72
+ return chalk.rgb(Math.round(r * Math.min(1, mult)), Math.round(g * Math.min(1, mult)), Math.round(b * Math.min(1, mult)))(char);
73
+ })
74
+ .join('');
75
+ const line = PREFIX + link(GITHUB_URL, styledAuthor);
76
+ out.write('\r\x1b[K' + line);
77
+ await sleep(frameMs);
78
+ }
79
+ const finalLine = PREFIX + link(GITHUB_URL, glow(AUTHOR, glowColor));
80
+ out.write('\r\x1b[K' + finalLine + '\n');
81
+ out.write('\n');
82
+ };
@@ -1,12 +1,18 @@
1
- import type { Rule } from '../types.js';
1
+ import type { Rule, RuleKind } from '../types.js';
2
2
  /**
3
- * Builds Cursor .mdc content for a rule: YAML frontmatter (description, globs, alwaysApply) then body.
3
+ * Builds Cursor .mdc content for a rule: YAML frontmatter (description, globs, alwaysApply, kind) then body.
4
4
  */
5
5
  export declare const buildRuleMdcContent: (rule: Rule) => string;
6
+ /**
7
+ * Builds .md content for solution, skill, or command with frontmatter (kind, description)
8
+ * so that bitcompass share can infer kind when re-pushing.
9
+ */
10
+ export declare const buildMarkdownWithKind: (rule: Rule) => string;
6
11
  export interface ParsedMdcFrontmatter {
7
12
  description: string;
8
13
  globs?: string;
9
14
  alwaysApply: boolean;
15
+ kind?: RuleKind;
10
16
  body: string;
11
17
  }
12
18
  /**
@@ -14,3 +20,8 @@ export interface ParsedMdcFrontmatter {
14
20
  * Returns null if the file does not start with --- (not frontmatter format).
15
21
  */
16
22
  export declare const parseRuleMdcContent: (raw: string) => ParsedMdcFrontmatter | null;
23
+ /**
24
+ * Returns kind from the first frontmatter block if present and valid.
25
+ * Use for .md or .mdc files when you only need kind (e.g. solution/skill/command round-trip).
26
+ */
27
+ export declare const parseFrontmatterKind: (raw: string) => RuleKind | null;
@@ -1,9 +1,11 @@
1
+ import { isValidRuleKind } from './share-types.js';
1
2
  const FRONTMATTER_DELIM = '---';
2
3
  /**
3
- * Builds Cursor .mdc content for a rule: YAML frontmatter (description, globs, alwaysApply) then body.
4
+ * Builds Cursor .mdc content for a rule: YAML frontmatter (description, globs, alwaysApply, kind) then body.
4
5
  */
5
6
  export const buildRuleMdcContent = (rule) => {
6
7
  const lines = [FRONTMATTER_DELIM];
8
+ lines.push(`kind: ${rule.kind}`);
7
9
  lines.push(`description: ${escapeYamlValue(rule.description ?? '')}`);
8
10
  if (rule.globs != null && String(rule.globs).trim() !== '') {
9
11
  lines.push(`globs: ${escapeYamlValue(String(rule.globs).trim())}`);
@@ -17,6 +19,35 @@ export const buildRuleMdcContent = (rule) => {
17
19
  }
18
20
  return lines.join('\n');
19
21
  };
22
+ /**
23
+ * Builds .md content for solution, skill, or command with frontmatter (kind, description)
24
+ * so that bitcompass share can infer kind when re-pushing.
25
+ */
26
+ export const buildMarkdownWithKind = (rule) => {
27
+ if (rule.kind === 'rule') {
28
+ return buildRuleMdcContent(rule);
29
+ }
30
+ const lines = [FRONTMATTER_DELIM];
31
+ lines.push(`kind: ${rule.kind}`);
32
+ lines.push(`description: ${escapeYamlValue(rule.description ?? '')}`);
33
+ lines.push(FRONTMATTER_DELIM);
34
+ lines.push('');
35
+ lines.push(`# ${rule.title}`);
36
+ lines.push('');
37
+ if (rule.description) {
38
+ lines.push(rule.description);
39
+ lines.push('');
40
+ }
41
+ if (rule.kind === 'solution') {
42
+ lines.push('## Solution');
43
+ lines.push('');
44
+ }
45
+ lines.push(rule.body.trimEnd());
46
+ if (!rule.body.endsWith('\n')) {
47
+ lines.push('');
48
+ }
49
+ return lines.join('\n');
50
+ };
20
51
  const escapeYamlValue = (s) => {
21
52
  if (/^[a-z0-9-]+$/i.test(s) && !s.includes(':'))
22
53
  return s;
@@ -41,6 +72,7 @@ export const parseRuleMdcContent = (raw) => {
41
72
  let description = '';
42
73
  let globs;
43
74
  let alwaysApply = false;
75
+ let kind;
44
76
  for (const line of frontmatterBlock.split('\n')) {
45
77
  const colonIdx = line.indexOf(':');
46
78
  if (colonIdx === -1)
@@ -60,7 +92,19 @@ export const parseRuleMdcContent = (raw) => {
60
92
  case 'alwaysApply':
61
93
  alwaysApply = value === 'true' || value === '1';
62
94
  break;
95
+ case 'kind':
96
+ if (isValidRuleKind(value))
97
+ kind = value;
98
+ break;
63
99
  }
64
100
  }
65
- return { description, globs, alwaysApply, body };
101
+ return { description, globs, alwaysApply, kind, body };
102
+ };
103
+ /**
104
+ * Returns kind from the first frontmatter block if present and valid.
105
+ * Use for .md or .mdc files when you only need kind (e.g. solution/skill/command round-trip).
106
+ */
107
+ export const parseFrontmatterKind = (raw) => {
108
+ const parsed = parseRuleMdcContent(raw);
109
+ return parsed?.kind ?? null;
66
110
  };
@@ -3,7 +3,7 @@ import { join } from 'path';
3
3
  import { getConfigDir } from '../auth/config.js';
4
4
  import { getRuleById } from '../api/client.js';
5
5
  import { ruleFilename, solutionFilename, skillFilename, commandFilename } from './slug.js';
6
- import { buildRuleMdcContent } from './mdc-format.js';
6
+ import { buildRuleMdcContent, buildMarkdownWithKind } from './mdc-format.js';
7
7
  /**
8
8
  * Gets the cache directory for rules (~/.bitcompass/cache/rules/)
9
9
  */
@@ -42,9 +42,7 @@ export const ensureRuleCached = async (id) => {
42
42
  if (needsUpdate) {
43
43
  const content = rule.kind === 'rule'
44
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`;
45
+ : buildMarkdownWithKind(rule);
48
46
  writeFileSync(cachedPath, content, 'utf-8');
49
47
  }
50
48
  return cachedPath;
@@ -5,7 +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
+ import { buildRuleMdcContent, buildMarkdownWithKind } from './mdc-format.js';
9
9
  /**
10
10
  * Pulls a rule or solution to a file using symbolic links (like Bun init).
11
11
  * Returns the file path where it was written/linked.
@@ -80,12 +80,10 @@ export const pullRuleToFile = async (id, options = {}) => {
80
80
  }
81
81
  }
82
82
  else {
83
- // Fallback: copy file content (rules as .mdc with frontmatter, others as .md)
83
+ // Fallback: copy file content (rules as .mdc with full frontmatter, others as .md with kind in frontmatter for round-trip)
84
84
  const content = rule.kind === 'rule'
85
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`;
86
+ : buildMarkdownWithKind(rule);
89
87
  writeFileSync(filename, content);
90
88
  }
91
89
  return filename;
@@ -0,0 +1,12 @@
1
+ import type { RuleKind } from '../types.js';
2
+ export declare const SHARE_KIND_CHOICES: Array<{
3
+ value: RuleKind;
4
+ name: string;
5
+ description: string;
6
+ }>;
7
+ export declare const isValidRuleKind: (s: string) => s is RuleKind;
8
+ /**
9
+ * Infers RuleKind from filename prefix (rule-*.mdc, solution-*.md, skill-*.md, command-*.md).
10
+ * Returns null if the basename does not match a known prefix.
11
+ */
12
+ export declare const inferKindFromFilename: (filePath: string) => RuleKind | null;
@@ -0,0 +1,41 @@
1
+ const VALID_KINDS = ['rule', 'solution', 'skill', 'command'];
2
+ export const SHARE_KIND_CHOICES = [
3
+ {
4
+ value: 'rule',
5
+ name: 'Rule',
6
+ description: 'Behaviors, documentation, or how-to for the AI (e.g. i18n guide, coding standards)',
7
+ },
8
+ {
9
+ value: 'solution',
10
+ name: 'Solution',
11
+ description: 'How we fixed or implemented a specific problem',
12
+ },
13
+ {
14
+ value: 'skill',
15
+ name: 'Skill',
16
+ description: 'How the AI should behave in a domain (e.g. front-end design, back-end implementation)',
17
+ },
18
+ {
19
+ value: 'command',
20
+ name: 'Command (workflow)',
21
+ description: 'A workflow or command (e.g. release checklist)',
22
+ },
23
+ ];
24
+ export const isValidRuleKind = (s) => VALID_KINDS.includes(s);
25
+ /**
26
+ * Infers RuleKind from filename prefix (rule-*.mdc, solution-*.md, skill-*.md, command-*.md).
27
+ * Returns null if the basename does not match a known prefix.
28
+ */
29
+ export const inferKindFromFilename = (filePath) => {
30
+ const base = filePath.split(/[/\\]/).pop() ?? '';
31
+ const lower = base.toLowerCase();
32
+ if (lower.startsWith('rule-') && (lower.endsWith('.mdc') || lower.endsWith('.md')))
33
+ return 'rule';
34
+ if (lower.startsWith('solution-') && (lower.endsWith('.md') || lower.endsWith('.mdc')))
35
+ return 'solution';
36
+ if (lower.startsWith('skill-') && (lower.endsWith('.md') || lower.endsWith('.mdc')))
37
+ return 'skill';
38
+ if (lower.startsWith('command-') && (lower.endsWith('.md') || lower.endsWith('.mdc')))
39
+ return 'command';
40
+ return null;
41
+ };
@@ -87,7 +87,7 @@ function createStdioServer() {
87
87
  },
88
88
  {
89
89
  name: 'post-rules',
90
- description: 'Use when the user wants to publish or share a new rule, solution, skill, or command to BitCompass. Requires kind, title, and body. Returns the created id and title on success. User must be logged in (bitcompass login).',
90
+ description: 'Use when the user wants to publish or share a new rule, solution, skill, or command to BitCompass. Requires kind, title, and body. When the user says "share" without specifying type, first ask or infer: Rule (behaviors, docs), Solution (how we fixed something), Skill (how AI should behave for X), Command (workflows). Returns the created id and title on success. User must be logged in (bitcompass login).',
91
91
  inputSchema: {
92
92
  type: 'object',
93
93
  properties: {
@@ -98,6 +98,7 @@ function createStdioServer() {
98
98
  context: { type: 'string' },
99
99
  examples: { type: 'array', items: { type: 'string' } },
100
100
  technologies: { type: 'array', items: { type: 'string' } },
101
+ project_id: { type: 'string', description: 'Optional: Compass project UUID to scope this rule to' },
101
102
  },
102
103
  required: ['kind', 'title', 'body'],
103
104
  },
@@ -121,18 +122,18 @@ function createStdioServer() {
121
122
  type: 'object',
122
123
  properties: {
123
124
  id: { type: 'string', description: 'Rule/solution ID' },
124
- kind: { type: 'string', enum: ['rule', 'solution'], description: 'Optional: filter by kind' },
125
+ kind: { type: 'string', enum: ['rule', 'solution', 'skill', 'command'], description: 'Optional: filter by kind' },
125
126
  },
126
127
  required: ['id'],
127
128
  },
128
129
  },
129
130
  {
130
131
  name: 'list-rules',
131
- description: 'Use when the user wants to browse or list all rules and/or solutions without a search query. Optional kind filter (rule or solution) and limit. Returns an array of items with id, title, kind, description, author, snippet, created_at, plus total/returned counts.',
132
+ description: 'Use when the user wants to browse or list all rules, solutions, skills, or commands without a search query. Optional kind filter (rule, solution, skill, command) and limit. Returns an array of items with id, title, kind, description, author, snippet, created_at, plus total/returned counts.',
132
133
  inputSchema: {
133
134
  type: 'object',
134
135
  properties: {
135
- kind: { type: 'string', enum: ['rule', 'solution'], description: 'Optional: filter by kind' },
136
+ kind: { type: 'string', enum: ['rule', 'solution', 'skill', 'command'], description: 'Optional: filter by kind' },
136
137
  limit: { type: 'number', description: 'Optional: maximum number of results (default: 50)' },
137
138
  },
138
139
  },
@@ -226,6 +227,7 @@ function createStdioServer() {
226
227
  id,
227
228
  result: {
228
229
  prompts: [
230
+ { name: 'share', title: 'Share something', description: 'Guide to publish a rule, solution, skill, or command. Asks what you\'re sharing, then collects content and publishes.' },
229
231
  { name: 'share_new_rule', title: 'Share a new rule', description: 'Guide to collect and publish a reusable rule' },
230
232
  { name: 'share_problem_solution', title: 'Share a problem solution', description: 'Guide to collect and publish a problem solution' },
231
233
  ],
@@ -236,6 +238,30 @@ function createStdioServer() {
236
238
  if (msg.method === 'prompts/get') {
237
239
  const params = msg.params;
238
240
  const name = params?.name ?? '';
241
+ if (name === 'share') {
242
+ send({
243
+ jsonrpc: '2.0',
244
+ id,
245
+ result: {
246
+ messages: [
247
+ {
248
+ role: 'user',
249
+ content: {
250
+ type: 'text',
251
+ text: `You are helping the user share something to BitCompass. First determine what they are sharing. If they already said (e.g. "share this rule", "share this workflow"), use that; otherwise ask with a single choice:
252
+ - Rule: Behaviors, documentation, or how-to for the AI (e.g. i18n guide, coding standards).
253
+ - Solution: How we fixed or implemented a specific problem.
254
+ - Skill: How the AI should behave in a domain (e.g. front-end design, back-end implementation).
255
+ - Command (workflow): A workflow or command (e.g. release checklist).
256
+
257
+ Then collect: title, description, body (and optionally context, examples, technologies). Ask one question at a time. Then call post-rules with the chosen kind and the collected fields.`,
258
+ },
259
+ },
260
+ ],
261
+ },
262
+ });
263
+ return;
264
+ }
239
265
  if (name === 'share_new_rule') {
240
266
  send({
241
267
  jsonrpc: '2.0',
@@ -352,6 +378,7 @@ function createStdioServer() {
352
378
  context: args.context || undefined,
353
379
  examples: Array.isArray(args.examples) ? args.examples : undefined,
354
380
  technologies: Array.isArray(args.technologies) ? args.technologies : undefined,
381
+ project_id: typeof args.project_id === 'string' ? args.project_id : undefined,
355
382
  };
356
383
  try {
357
384
  const created = await insertRule(payload);
package/dist/types.d.ts CHANGED
@@ -10,6 +10,8 @@ export interface Rule {
10
10
  technologies?: string[];
11
11
  user_id: string;
12
12
  author_display_name?: string | null;
13
+ /** When set, rule is scoped to this Compass project. */
14
+ project_id?: string | null;
13
15
  version?: string | null;
14
16
  /** Optional glob patterns for when the rule applies (e.g. "*.ts, *.tsx"). Used in .mdc frontmatter. */
15
17
  globs?: string | null;
@@ -26,6 +28,7 @@ export interface RuleInsert {
26
28
  context?: string | null;
27
29
  examples?: string[];
28
30
  technologies?: string[];
31
+ project_id?: string | null;
29
32
  version?: string;
30
33
  globs?: string | null;
31
34
  always_apply?: boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bitcompass",
3
- "version": "0.3.9",
3
+ "version": "0.4.1",
4
4
  "description": "BitCompass CLI - rules, solutions, and MCP server",
5
5
  "type": "module",
6
6
  "bin": {
@@ -65,25 +65,29 @@ const glowWithBrightness = (text, [r, g, b], brightness) =>
65
65
 
66
66
  const cyan = [0, 212, 255];
67
67
  const magenta = [255, 64, 200];
68
- const amber = [255, 190, 50];
69
68
  const glowColor = [255, 120, 255];
70
69
 
71
70
  const PREFIX = "made by ";
72
- const AUTHOR = "davide97g";
71
+ const AUTHOR = "@davide97g";
73
72
  const GITHUB_URL = "https://github.com/davide97g/";
73
+ const INIT_TIP =
74
+ chalk.dim("Run ") + chalk.cyan("bitcompass init") + chalk.dim(" in your project to configure.");
74
75
 
75
76
  /** Wraps text in OSC 8 hyperlink so it's clickable in supported terminals (VS Code, iTerm2, Windows Terminal). */
76
77
  const link = (url, text) => `\x1b]8;;${url}\x1b\\${text}\x1b]8;;\x1b\\`;
77
78
 
78
79
  const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
79
80
 
81
+ /** npm v7+ runs lifecycle scripts in background and suppresses stdout. Write to stderr so the message is visible during `npm i bitcompass -g`. */
82
+ const out = process.stderr;
83
+
80
84
  const animateAuthorLine = async () => {
81
- const isTty = process.stdout.isTTY;
85
+ const isTty = out.isTTY;
82
86
  const frames = 6;
83
87
  const frameMs = 80;
84
88
 
85
89
  if (!isTty) {
86
- console.log(PREFIX + link(GITHUB_URL, glow(AUTHOR, glowColor)));
90
+ out.write(PREFIX + link(GITHUB_URL, glow(AUTHOR, glowColor)) + "\n");
87
91
  return;
88
92
  }
89
93
 
@@ -92,16 +96,15 @@ const animateAuthorLine = async () => {
92
96
  const brightness = 0.3 + 0.7 * (1 - Math.cos(t * Math.PI));
93
97
  const styledAuthor = glowWithBrightness(AUTHOR, glowColor, brightness);
94
98
  const line = PREFIX + link(GITHUB_URL, styledAuthor);
95
- process.stdout.write("\r\x1b[K" + line);
99
+ out.write("\r\x1b[K" + line);
96
100
  await sleep(frameMs);
97
101
  }
98
102
  const finalLine = PREFIX + link(GITHUB_URL, glow(AUTHOR, glowColor));
99
- process.stdout.write("\r\x1b[K" + finalLine + "\n");
103
+ out.write("\r\x1b[K" + finalLine + "\n");
100
104
  };
101
105
 
102
- (async () => {
103
- console.log("");
104
- console.log(gradient(`version ${version}`, cyan, magenta));
105
- await animateAuthorLine();
106
- console.log("");
107
- })();
106
+ out.write("\n");
107
+ out.write(gradient(`version ${version}`, cyan, magenta) + "\n");
108
+ await animateAuthorLine();
109
+ out.write(INIT_TIP + "\n");
110
+ out.write("\n");