bitcompass 0.3.4 → 0.3.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.
@@ -0,0 +1,7 @@
1
+ export declare const runCommandsSearch: (query?: string) => Promise<void>;
2
+ export declare const runCommandsList: () => Promise<void>;
3
+ export declare const runCommandsPull: (id?: string, options?: {
4
+ global?: boolean;
5
+ copy?: boolean;
6
+ }) => Promise<void>;
7
+ export declare const runCommandsPush: (file?: string) => Promise<void>;
@@ -0,0 +1,120 @@
1
+ import inquirer from 'inquirer';
2
+ import ora from 'ora';
3
+ import chalk from 'chalk';
4
+ import { loadCredentials } from '../auth/config.js';
5
+ import { searchRules, fetchRules, getRuleById, insertRule } from '../api/client.js';
6
+ import { pullRuleToFile } from '../lib/rule-file-ops.js';
7
+ export const runCommandsSearch = async (query) => {
8
+ if (!loadCredentials()) {
9
+ console.error(chalk.red('Not logged in. Run bitcompass login.'));
10
+ process.exit(1);
11
+ }
12
+ const q = query ?? (await inquirer.prompt([{ name: 'q', message: 'Search query', type: 'input' }])).q;
13
+ const spinner = ora('Searching commands…').start();
14
+ const list = await searchRules(q, { kind: 'command', limit: 20 });
15
+ spinner.stop();
16
+ if (list.length === 0) {
17
+ console.log(chalk.yellow('No commands found.'));
18
+ return;
19
+ }
20
+ const choice = await inquirer.prompt([
21
+ {
22
+ name: 'id',
23
+ message: 'Select a command',
24
+ type: 'list',
25
+ choices: list.map((r) => ({ name: `${r.title} (${r.id})`, value: r.id })),
26
+ },
27
+ ]);
28
+ const rule = await getRuleById(choice.id);
29
+ if (rule) {
30
+ console.log(chalk.cyan(rule.title));
31
+ console.log(rule.body);
32
+ }
33
+ };
34
+ export const runCommandsList = async () => {
35
+ if (!loadCredentials()) {
36
+ console.error(chalk.red('Not logged in. Run bitcompass login.'));
37
+ process.exit(1);
38
+ }
39
+ const spinner = ora('Loading commands…').start();
40
+ const list = await fetchRules('command');
41
+ spinner.stop();
42
+ list.forEach((r) => console.log(`${chalk.cyan(r.title)} ${chalk.dim(r.id)}`));
43
+ if (list.length === 0)
44
+ console.log(chalk.yellow('No commands yet.'));
45
+ };
46
+ export const runCommandsPull = async (id, options) => {
47
+ if (!loadCredentials()) {
48
+ console.error(chalk.red('Not logged in. Run bitcompass login.'));
49
+ process.exit(1);
50
+ }
51
+ let targetId = id;
52
+ if (!targetId) {
53
+ const spinner = ora('Loading commands…').start();
54
+ const list = await fetchRules('command');
55
+ spinner.stop();
56
+ if (list.length === 0) {
57
+ console.log(chalk.yellow('No commands to pull.'));
58
+ return;
59
+ }
60
+ const choice = await inquirer.prompt([
61
+ { name: 'id', message: 'Select command', type: 'list', choices: list.map((r) => ({ name: r.title, value: r.id })) },
62
+ ]);
63
+ targetId = choice.id;
64
+ }
65
+ const spinner = ora('Pulling command…').start();
66
+ try {
67
+ const filename = await pullRuleToFile(targetId, {
68
+ global: options?.global,
69
+ useSymlink: !options?.copy, // Use symlink unless --copy flag is set
70
+ });
71
+ spinner.succeed(chalk.green('Pulled command'));
72
+ console.log(chalk.dim(filename));
73
+ if (options?.copy) {
74
+ console.log(chalk.dim('Copied as file (not a symlink)'));
75
+ }
76
+ else {
77
+ console.log(chalk.dim('Created symbolic link to cached command'));
78
+ }
79
+ if (options?.global) {
80
+ console.log(chalk.dim('Installed globally for all projects'));
81
+ }
82
+ }
83
+ catch (error) {
84
+ spinner.fail(chalk.red('Failed to pull command'));
85
+ console.error(chalk.red(error.message));
86
+ process.exit(1);
87
+ }
88
+ };
89
+ export const runCommandsPush = async (file) => {
90
+ if (!loadCredentials()) {
91
+ console.error(chalk.red('Not logged in. Run bitcompass login.'));
92
+ process.exit(1);
93
+ }
94
+ let payload;
95
+ if (file) {
96
+ const { readFileSync } = await import('fs');
97
+ const raw = readFileSync(file, 'utf-8');
98
+ try {
99
+ payload = JSON.parse(raw);
100
+ payload.kind = 'command';
101
+ }
102
+ catch {
103
+ const lines = raw.split('\n');
104
+ const title = lines[0].replace(/^#\s*/, '') || 'Untitled';
105
+ payload = { kind: 'command', title, description: '', body: raw };
106
+ }
107
+ }
108
+ else {
109
+ const answers = await inquirer.prompt([
110
+ { name: 'title', message: 'Command title', type: 'input', default: 'Untitled' },
111
+ { name: 'description', message: 'Description', type: 'input', default: '' },
112
+ { name: 'body', message: 'Command content', type: 'editor', default: '' },
113
+ ]);
114
+ payload = { kind: 'command', title: answers.title, description: answers.description, body: answers.body };
115
+ }
116
+ const spinner = ora('Publishing command…').start();
117
+ const created = await insertRule(payload);
118
+ spinner.succeed(chalk.green('Published command ') + created.id);
119
+ console.log(chalk.dim(created.title));
120
+ };
@@ -0,0 +1,7 @@
1
+ export declare const runSkillsSearch: (query?: string) => Promise<void>;
2
+ export declare const runSkillsList: () => Promise<void>;
3
+ export declare const runSkillsPull: (id?: string, options?: {
4
+ global?: boolean;
5
+ copy?: boolean;
6
+ }) => Promise<void>;
7
+ export declare const runSkillsPush: (file?: string) => Promise<void>;
@@ -0,0 +1,120 @@
1
+ import inquirer from 'inquirer';
2
+ import ora from 'ora';
3
+ import chalk from 'chalk';
4
+ import { loadCredentials } from '../auth/config.js';
5
+ import { searchRules, fetchRules, getRuleById, insertRule } from '../api/client.js';
6
+ import { pullRuleToFile } from '../lib/rule-file-ops.js';
7
+ export const runSkillsSearch = async (query) => {
8
+ if (!loadCredentials()) {
9
+ console.error(chalk.red('Not logged in. Run bitcompass login.'));
10
+ process.exit(1);
11
+ }
12
+ const q = query ?? (await inquirer.prompt([{ name: 'q', message: 'Search query', type: 'input' }])).q;
13
+ const spinner = ora('Searching skills…').start();
14
+ const list = await searchRules(q, { kind: 'skill', limit: 20 });
15
+ spinner.stop();
16
+ if (list.length === 0) {
17
+ console.log(chalk.yellow('No skills found.'));
18
+ return;
19
+ }
20
+ const choice = await inquirer.prompt([
21
+ {
22
+ name: 'id',
23
+ message: 'Select a skill',
24
+ type: 'list',
25
+ choices: list.map((r) => ({ name: `${r.title} (${r.id})`, value: r.id })),
26
+ },
27
+ ]);
28
+ const rule = await getRuleById(choice.id);
29
+ if (rule) {
30
+ console.log(chalk.cyan(rule.title));
31
+ console.log(rule.body);
32
+ }
33
+ };
34
+ export const runSkillsList = async () => {
35
+ if (!loadCredentials()) {
36
+ console.error(chalk.red('Not logged in. Run bitcompass login.'));
37
+ process.exit(1);
38
+ }
39
+ const spinner = ora('Loading skills…').start();
40
+ const list = await fetchRules('skill');
41
+ spinner.stop();
42
+ list.forEach((r) => console.log(`${chalk.cyan(r.title)} ${chalk.dim(r.id)}`));
43
+ if (list.length === 0)
44
+ console.log(chalk.yellow('No skills yet.'));
45
+ };
46
+ export const runSkillsPull = async (id, options) => {
47
+ if (!loadCredentials()) {
48
+ console.error(chalk.red('Not logged in. Run bitcompass login.'));
49
+ process.exit(1);
50
+ }
51
+ let targetId = id;
52
+ if (!targetId) {
53
+ const spinner = ora('Loading skills…').start();
54
+ const list = await fetchRules('skill');
55
+ spinner.stop();
56
+ if (list.length === 0) {
57
+ console.log(chalk.yellow('No skills to pull.'));
58
+ return;
59
+ }
60
+ const choice = await inquirer.prompt([
61
+ { name: 'id', message: 'Select skill', type: 'list', choices: list.map((r) => ({ name: r.title, value: r.id })) },
62
+ ]);
63
+ targetId = choice.id;
64
+ }
65
+ const spinner = ora('Pulling skill…').start();
66
+ try {
67
+ const filename = await pullRuleToFile(targetId, {
68
+ global: options?.global,
69
+ useSymlink: !options?.copy, // Use symlink unless --copy flag is set
70
+ });
71
+ spinner.succeed(chalk.green('Pulled skill'));
72
+ console.log(chalk.dim(filename));
73
+ if (options?.copy) {
74
+ console.log(chalk.dim('Copied as file (not a symlink)'));
75
+ }
76
+ else {
77
+ console.log(chalk.dim('Created symbolic link to cached skill'));
78
+ }
79
+ if (options?.global) {
80
+ console.log(chalk.dim('Installed globally for all projects'));
81
+ }
82
+ }
83
+ catch (error) {
84
+ spinner.fail(chalk.red('Failed to pull skill'));
85
+ console.error(chalk.red(error.message));
86
+ process.exit(1);
87
+ }
88
+ };
89
+ export const runSkillsPush = async (file) => {
90
+ if (!loadCredentials()) {
91
+ console.error(chalk.red('Not logged in. Run bitcompass login.'));
92
+ process.exit(1);
93
+ }
94
+ let payload;
95
+ if (file) {
96
+ const { readFileSync } = await import('fs');
97
+ const raw = readFileSync(file, 'utf-8');
98
+ try {
99
+ payload = JSON.parse(raw);
100
+ payload.kind = 'skill';
101
+ }
102
+ catch {
103
+ const lines = raw.split('\n');
104
+ const title = lines[0].replace(/^#\s*/, '') || 'Untitled';
105
+ payload = { kind: 'skill', title, description: '', body: raw };
106
+ }
107
+ }
108
+ else {
109
+ const answers = await inquirer.prompt([
110
+ { name: 'title', message: 'Skill title', type: 'input', default: 'Untitled' },
111
+ { name: 'description', message: 'Description', type: 'input', default: '' },
112
+ { name: 'body', message: 'Skill content', type: 'editor', default: '' },
113
+ ]);
114
+ payload = { kind: 'skill', title: answers.title, description: answers.description, body: answers.body };
115
+ }
116
+ const spinner = ora('Publishing skill…').start();
117
+ const created = await insertRule(payload);
118
+ spinner.succeed(chalk.green('Published skill ') + created.id);
119
+ console.log(chalk.dim(created.title));
120
+ };
package/dist/index.js CHANGED
@@ -13,6 +13,8 @@ import { runMcpStart, runMcpStatus } from './commands/mcp.js';
13
13
  import { runLog } from './commands/log.js';
14
14
  import { runRulesList, runRulesPull, runRulesPush, runRulesSearch } from './commands/rules.js';
15
15
  import { runSolutionsPull, runSolutionsPush, runSolutionsSearch } from './commands/solutions.js';
16
+ import { runSkillsList, runSkillsPull, runSkillsPush, runSkillsSearch } from './commands/skills.js';
17
+ import { runCommandsList, runCommandsPull, runCommandsPush, runCommandsSearch } from './commands/commands.js';
16
18
  import { runWhoami } from './commands/whoami.js';
17
19
  // Read version from package.json
18
20
  const __dirname = dirname(fileURLToPath(import.meta.url));
@@ -70,6 +72,28 @@ solutions
70
72
  .option('--copy', 'Copy file instead of creating symbolic link')
71
73
  .action((id, options) => runSolutionsPull(id, options).catch(handleErr));
72
74
  solutions.command('push [file]').description('Push a solution (file or interactive)').action((file) => runSolutionsPush(file).catch(handleErr));
75
+ // skills
76
+ const skills = program.command('skills').description('Manage skills');
77
+ skills.command('search [query]').description('Search skills').action((query) => runSkillsSearch(query).catch(handleErr));
78
+ skills.command('list').description('List skills').action(() => runSkillsList().catch(handleErr));
79
+ skills
80
+ .command('pull [id]')
81
+ .description('Pull a skill by ID or choose from list (creates symbolic link by default)')
82
+ .option('-g, --global', 'Install globally to ~/.cursor/rules/ for all projects')
83
+ .option('--copy', 'Copy file instead of creating symbolic link')
84
+ .action((id, options) => runSkillsPull(id, options).catch(handleErr));
85
+ skills.command('push [file]').description('Push a skill (file or interactive)').action((file) => runSkillsPush(file).catch(handleErr));
86
+ // commands
87
+ const commands = program.command('commands').description('Manage commands');
88
+ commands.command('search [query]').description('Search commands').action((query) => runCommandsSearch(query).catch(handleErr));
89
+ commands.command('list').description('List commands').action(() => runCommandsList().catch(handleErr));
90
+ commands
91
+ .command('pull [id]')
92
+ .description('Pull a command by ID or choose from list (creates symbolic link by default)')
93
+ .option('-g, --global', 'Install globally to ~/.cursor/rules/ for all projects')
94
+ .option('--copy', 'Copy file instead of creating symbolic link')
95
+ .action((id, options) => runCommandsPull(id, options).catch(handleErr));
96
+ commands.command('push [file]').description('Push a command (file or interactive)').action((file) => runCommandsPush(file).catch(handleErr));
73
97
  // mcp
74
98
  const mcp = program.command('mcp').description('MCP server');
75
99
  mcp.command('start').description('Start MCP server (stdio)').action(() => runMcpStart().catch(handleErr));
@@ -3,7 +3,7 @@ import { join, relative, dirname } from 'path';
3
3
  import { homedir } from 'os';
4
4
  import { getRuleById } from '../api/client.js';
5
5
  import { getProjectConfig } from '../auth/project-config.js';
6
- import { ruleFilename, solutionFilename } from './slug.js';
6
+ import { ruleFilename, solutionFilename, skillFilename, commandFilename } from './slug.js';
7
7
  import { ensureRuleCached } from './rule-cache.js';
8
8
  /**
9
9
  * Pulls a rule or solution to a file using symbolic links (like Bun init).
@@ -33,9 +33,22 @@ export const pullRuleToFile = async (id, options = {}) => {
33
33
  outDir = join(process.cwd(), outputPath);
34
34
  }
35
35
  mkdirSync(outDir, { recursive: true });
36
- const filename = rule.kind === 'solution'
37
- ? join(outDir, solutionFilename(rule.title, rule.id))
38
- : join(outDir, ruleFilename(rule.title, rule.id));
36
+ let filename;
37
+ switch (rule.kind) {
38
+ case 'solution':
39
+ filename = join(outDir, solutionFilename(rule.title, rule.id));
40
+ break;
41
+ case 'skill':
42
+ filename = join(outDir, skillFilename(rule.title, rule.id));
43
+ break;
44
+ case 'command':
45
+ filename = join(outDir, commandFilename(rule.title, rule.id));
46
+ break;
47
+ case 'rule':
48
+ default:
49
+ filename = join(outDir, ruleFilename(rule.title, rule.id));
50
+ break;
51
+ }
39
52
  // Remove existing file/symlink if it exists
40
53
  if (existsSync(filename)) {
41
54
  try {
@@ -67,9 +80,22 @@ export const pullRuleToFile = async (id, options = {}) => {
67
80
  }
68
81
  else {
69
82
  // Fallback: copy file content (for compatibility or when symlinks aren't desired)
70
- const content = rule.kind === 'solution'
71
- ? `# ${rule.title}\n\n${rule.description}\n\n## Solution\n\n${rule.body}\n`
72
- : `# ${rule.title}\n\n${rule.description}\n\n${rule.body}\n`;
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
+ }
73
99
  writeFileSync(filename, content);
74
100
  }
75
101
  return filename;
@@ -13,3 +13,13 @@ export declare const ruleFilename: (title: string, id: string) => string;
13
13
  * Falls back to id if slug is empty.
14
14
  */
15
15
  export declare const solutionFilename: (title: string, id: string) => string;
16
+ /**
17
+ * Returns the skill filename (e.g. skill-strava-api-authentication-flow.md).
18
+ * Falls back to id if slug is empty.
19
+ */
20
+ export declare const skillFilename: (title: string, id: string) => string;
21
+ /**
22
+ * Returns the command filename (e.g. command-strava-api-authentication-flow.md).
23
+ * Falls back to id if slug is empty.
24
+ */
25
+ export declare const commandFilename: (title: string, id: string) => string;
package/dist/lib/slug.js CHANGED
@@ -31,3 +31,21 @@ export const solutionFilename = (title, id) => {
31
31
  const base = slug ? `solution-${slug}` : `solution-${id}`;
32
32
  return `${base}.md`;
33
33
  };
34
+ /**
35
+ * Returns the skill filename (e.g. skill-strava-api-authentication-flow.md).
36
+ * Falls back to id if slug is empty.
37
+ */
38
+ export const skillFilename = (title, id) => {
39
+ const slug = titleToSlug(title);
40
+ const base = slug ? `skill-${slug}` : `skill-${id}`;
41
+ return `${base}.md`;
42
+ };
43
+ /**
44
+ * Returns the command filename (e.g. command-strava-api-authentication-flow.md).
45
+ * Falls back to id if slug is empty.
46
+ */
47
+ export const commandFilename = (title, id) => {
48
+ const slug = titleToSlug(title);
49
+ const base = slug ? `command-${slug}` : `command-${id}`;
50
+ return `${base}.md`;
51
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bitcompass",
3
- "version": "0.3.4",
3
+ "version": "0.3.5",
4
4
  "description": "BitCompass CLI - rules, solutions, and MCP server",
5
5
  "type": "module",
6
6
  "bin": {