bitcompass 0.3.4 → 0.3.6

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,11 @@
1
+ export declare const runCommandsSearch: (query?: string, options?: {
2
+ listOnly?: boolean;
3
+ }) => Promise<void>;
4
+ export declare const runCommandsList: (options?: {
5
+ table?: boolean;
6
+ }) => Promise<void>;
7
+ export declare const runCommandsPull: (id?: string, options?: {
8
+ global?: boolean;
9
+ copy?: boolean;
10
+ }) => Promise<void>;
11
+ export declare const runCommandsPush: (file?: string) => Promise<void>;
@@ -0,0 +1,129 @@
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
+ import { formatList, shouldUseTable } from '../lib/list-format.js';
8
+ export const runCommandsSearch = async (query, options) => {
9
+ if (!loadCredentials()) {
10
+ console.error(chalk.red('Not logged in. Run bitcompass login.'));
11
+ process.exit(1);
12
+ }
13
+ const q = query ?? (await inquirer.prompt([{ name: 'q', message: 'Search query', type: 'input' }])).q;
14
+ const spinner = ora('Searching commands…').start();
15
+ const list = await searchRules(q, { kind: 'command', limit: 20 });
16
+ spinner.stop();
17
+ if (list.length === 0) {
18
+ console.log(chalk.yellow('No commands found.'));
19
+ return;
20
+ }
21
+ if (options?.listOnly) {
22
+ formatList(list.map((r) => ({ id: r.id, title: r.title, kind: r.kind })), { useTable: shouldUseTable(), showKind: true });
23
+ return;
24
+ }
25
+ const choice = await inquirer.prompt([
26
+ {
27
+ name: 'id',
28
+ message: 'Select a command',
29
+ type: 'list',
30
+ choices: list.map((r) => ({ name: `${r.title} (${r.id})`, value: r.id })),
31
+ },
32
+ ]);
33
+ const rule = await getRuleById(choice.id);
34
+ if (rule) {
35
+ console.log(chalk.cyan(rule.title));
36
+ console.log(rule.body);
37
+ }
38
+ };
39
+ export const runCommandsList = async (options) => {
40
+ if (!loadCredentials()) {
41
+ console.error(chalk.red('Not logged in. Run bitcompass login.'));
42
+ process.exit(1);
43
+ }
44
+ const spinner = ora('Loading commands…').start();
45
+ const list = await fetchRules('command');
46
+ spinner.stop();
47
+ if (list.length === 0) {
48
+ console.log(chalk.yellow('No commands yet.'));
49
+ return;
50
+ }
51
+ const useTable = shouldUseTable(options?.table);
52
+ formatList(list.map((r) => ({ id: r.id, title: r.title, kind: r.kind })), { useTable, showKind: false });
53
+ };
54
+ export const runCommandsPull = async (id, options) => {
55
+ if (!loadCredentials()) {
56
+ console.error(chalk.red('Not logged in. Run bitcompass login.'));
57
+ process.exit(1);
58
+ }
59
+ let targetId = id;
60
+ if (!targetId) {
61
+ const spinner = ora('Loading commands…').start();
62
+ const list = await fetchRules('command');
63
+ spinner.stop();
64
+ if (list.length === 0) {
65
+ console.log(chalk.yellow('No commands to pull.'));
66
+ return;
67
+ }
68
+ const choice = await inquirer.prompt([
69
+ { name: 'id', message: 'Select command', type: 'list', choices: list.map((r) => ({ name: r.title, value: r.id })) },
70
+ ]);
71
+ targetId = choice.id;
72
+ }
73
+ const spinner = ora('Pulling command…').start();
74
+ try {
75
+ const filename = await pullRuleToFile(targetId, {
76
+ global: options?.global,
77
+ useSymlink: !options?.copy, // Use symlink unless --copy flag is set
78
+ });
79
+ spinner.succeed(chalk.green('Pulled command'));
80
+ console.log(chalk.dim(filename));
81
+ if (options?.copy) {
82
+ console.log(chalk.dim('Copied as file (not a symlink)'));
83
+ }
84
+ else {
85
+ console.log(chalk.dim('Created symbolic link to cached command'));
86
+ }
87
+ if (options?.global) {
88
+ console.log(chalk.dim('Installed globally for all projects'));
89
+ }
90
+ }
91
+ catch (error) {
92
+ spinner.fail(chalk.red('Failed to pull command'));
93
+ const message = error instanceof Error ? error.message : String(error);
94
+ console.error(chalk.red(message));
95
+ process.exit(message.includes('not found') ? 2 : 1);
96
+ }
97
+ };
98
+ export const runCommandsPush = 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 = 'command';
110
+ }
111
+ catch {
112
+ const lines = raw.split('\n');
113
+ const title = lines[0].replace(/^#\s*/, '') || 'Untitled';
114
+ payload = { kind: 'command', title, description: '', body: raw };
115
+ }
116
+ }
117
+ else {
118
+ const answers = await inquirer.prompt([
119
+ { name: 'title', message: 'Command title', type: 'input', default: 'Untitled' },
120
+ { name: 'description', message: 'Description', type: 'input', default: '' },
121
+ { name: 'body', message: 'Command content', type: 'editor', default: '' },
122
+ ]);
123
+ payload = { kind: 'command', title: answers.title, description: answers.description, body: answers.body };
124
+ }
125
+ const spinner = ora('Publishing command…').start();
126
+ const created = await insertRule(payload);
127
+ spinner.succeed(chalk.green('Published command ') + created.id);
128
+ console.log(chalk.dim(created.title));
129
+ };
@@ -16,7 +16,7 @@ export const runConfigSet = (key, value) => {
16
16
  const config = loadConfig();
17
17
  if (!CONFIG_KEYS.includes(key)) {
18
18
  console.error(chalk.red('Unknown key. Use one of:'), CONFIG_KEYS.join(', '));
19
- process.exit(1);
19
+ process.exit(2);
20
20
  }
21
21
  config[key] = value;
22
22
  saveConfig(config);
@@ -26,7 +26,7 @@ export const runConfigGet = (key) => {
26
26
  const config = loadConfig();
27
27
  if (!CONFIG_KEYS.includes(key)) {
28
28
  console.error(chalk.red('Unknown key.'));
29
- process.exit(1);
29
+ process.exit(2);
30
30
  }
31
31
  const val = config[key] ?? process.env[`BITCOMPASS_${key.toUpperCase()}`];
32
32
  console.log(val ?? '');
@@ -0,0 +1 @@
1
+ export declare const runGlossary: () => void;
@@ -0,0 +1,16 @@
1
+ import chalk from 'chalk';
2
+ import { readFileSync, existsSync } from 'fs';
3
+ import { dirname, join } from 'path';
4
+ import { fileURLToPath } from 'url';
5
+ const __dirname = dirname(fileURLToPath(import.meta.url));
6
+ /** Path to glossary.md in the CLI package (works when run from source or installed). */
7
+ const getGlossaryPath = () => join(__dirname, '..', '..', 'glossary.md');
8
+ export const runGlossary = () => {
9
+ const path = getGlossaryPath();
10
+ if (!existsSync(path)) {
11
+ console.error(chalk.red('Glossary file not found.'));
12
+ process.exit(1);
13
+ }
14
+ const content = readFileSync(path, 'utf8');
15
+ console.log(content.trim());
16
+ };
@@ -1,9 +1,11 @@
1
1
  import type { TimeFrame } from '../lib/git-analysis.js';
2
+ export type LogProgressStep = 'analyzing' | 'pushing';
2
3
  /**
3
4
  * Shared logic: resolve repo, compute period, gather summary + git analysis, insert log.
4
5
  * Used by both CLI and MCP. Returns the created log id or throws.
6
+ * Optional onProgress callback for CLI to show step-wise spinner (e.g. analyzing → pushing).
5
7
  */
6
- export declare const buildAndPushActivityLog: (timeFrame: TimeFrame, cwd: string) => Promise<{
8
+ export declare const buildAndPushActivityLog: (timeFrame: TimeFrame, cwd: string, onProgress?: (step: LogProgressStep) => void) => Promise<{
7
9
  id: string;
8
10
  }>;
9
11
  /**
@@ -13,7 +15,7 @@ export declare const buildAndPushActivityLogWithPeriod: (period: {
13
15
  period_start: string;
14
16
  period_end: string;
15
17
  since: string;
16
- }, timeFrame: TimeFrame, cwd: string) => Promise<{
18
+ }, timeFrame: TimeFrame, cwd: string, onProgress?: (step: LogProgressStep) => void) => Promise<{
17
19
  id: string;
18
20
  }>;
19
21
  /** Parse argv for log: [start] or [start, end] or [start, '-', end]. Returns { start, end } or null for interactive. */
@@ -21,4 +23,8 @@ export declare const parseLogArgs: (args: string[]) => {
21
23
  start: string;
22
24
  end?: string;
23
25
  } | null;
26
+ /** Thrown for invalid date args; CLI should exit with code 2. */
27
+ export declare class ValidationError extends Error {
28
+ constructor(message: string);
29
+ }
24
30
  export declare const runLog: (args?: string[]) => Promise<void>;
@@ -1,18 +1,21 @@
1
1
  import inquirer from 'inquirer';
2
+ import ora from 'ora';
2
3
  import chalk from 'chalk';
3
4
  import { insertActivityLog } from '../api/client.js';
4
5
  import { loadCredentials } from '../auth/config.js';
5
- import { getRepoRoot, getRepoSummary, getGitAnalysis, getPeriodForTimeFrame, getPeriodForCustomDates, } from '../lib/git-analysis.js';
6
+ import { getRepoRoot, getRepoSummary, getGitAnalysis, getPeriodForTimeFrame, getPeriodForCustomDates, parseDate, } from '../lib/git-analysis.js';
6
7
  /**
7
8
  * Shared logic: resolve repo, compute period, gather summary + git analysis, insert log.
8
9
  * Used by both CLI and MCP. Returns the created log id or throws.
10
+ * Optional onProgress callback for CLI to show step-wise spinner (e.g. analyzing → pushing).
9
11
  */
10
- export const buildAndPushActivityLog = async (timeFrame, cwd) => {
12
+ export const buildAndPushActivityLog = async (timeFrame, cwd, onProgress) => {
11
13
  const repoRoot = getRepoRoot(cwd);
12
14
  if (!repoRoot) {
13
15
  throw new Error('Not a git repository. Run from a project with git or pass a valid repo path.');
14
16
  }
15
17
  const period = getPeriodForTimeFrame(timeFrame);
18
+ onProgress?.('analyzing');
16
19
  const repo_summary = getRepoSummary(repoRoot);
17
20
  const git_analysis = getGitAnalysis(repoRoot, period.since);
18
21
  const payload = {
@@ -22,17 +25,19 @@ export const buildAndPushActivityLog = async (timeFrame, cwd) => {
22
25
  repo_summary: repo_summary,
23
26
  git_analysis: git_analysis,
24
27
  };
28
+ onProgress?.('pushing');
25
29
  const created = await insertActivityLog(payload);
26
30
  return { id: created.id };
27
31
  };
28
32
  /**
29
33
  * Push an activity log for a custom date or date range. timeFrame is used for display (day/week/month).
30
34
  */
31
- export const buildAndPushActivityLogWithPeriod = async (period, timeFrame, cwd) => {
35
+ export const buildAndPushActivityLogWithPeriod = async (period, timeFrame, cwd, onProgress) => {
32
36
  const repoRoot = getRepoRoot(cwd);
33
37
  if (!repoRoot) {
34
38
  throw new Error('Not a git repository. Run from a project with git or pass a valid repo path.');
35
39
  }
40
+ onProgress?.('analyzing');
36
41
  const repo_summary = getRepoSummary(repoRoot);
37
42
  const git_analysis = getGitAnalysis(repoRoot, period.since, period.period_end);
38
43
  const payload = {
@@ -42,6 +47,7 @@ export const buildAndPushActivityLogWithPeriod = async (period, timeFrame, cwd)
42
47
  repo_summary: repo_summary,
43
48
  git_analysis: git_analysis,
44
49
  };
50
+ onProgress?.('pushing');
45
51
  const created = await insertActivityLog(payload);
46
52
  return { id: created.id };
47
53
  };
@@ -62,6 +68,13 @@ export const parseLogArgs = (args) => {
62
68
  }
63
69
  throw new Error('Usage: bitcompass log [YYYY-MM-DD] or bitcompass log [YYYY-MM-DD] [YYYY-MM-DD] or bitcompass log [YYYY-MM-DD] - [YYYY-MM-DD]');
64
70
  };
71
+ /** Thrown for invalid date args; CLI should exit with code 2. */
72
+ export class ValidationError extends Error {
73
+ constructor(message) {
74
+ super(message);
75
+ this.name = 'ValidationError';
76
+ }
77
+ }
65
78
  /** Choose time_frame for a custom range by span (day ≤ 1, week ≤ 7, else month). */
66
79
  const timeFrameForRange = (start, end) => {
67
80
  const a = new Date(start);
@@ -85,13 +98,33 @@ export const runLog = async (args = []) => {
85
98
  process.exit(1);
86
99
  }
87
100
  const parsed = parseLogArgs(args);
101
+ const spinner = ora('Analyzing repository…').start();
102
+ const onProgress = (step) => {
103
+ spinner.text = step === 'analyzing' ? 'Analyzing repository…' : 'Pushing activity log…';
104
+ };
88
105
  if (parsed) {
106
+ if (!parseDate(parsed.start)) {
107
+ spinner.stop();
108
+ throw new ValidationError(`Invalid date "${parsed.start}". Use YYYY-MM-DD (e.g. 2025-02-06).`);
109
+ }
110
+ if (parsed.end !== undefined && !parseDate(parsed.end)) {
111
+ spinner.stop();
112
+ throw new ValidationError(`Invalid date "${parsed.end}". Use YYYY-MM-DD (e.g. 2025-02-06).`);
113
+ }
89
114
  const period = getPeriodForCustomDates(parsed.start, parsed.end);
90
115
  const timeFrame = parsed.end ? timeFrameForRange(parsed.start, parsed.end) : 'day';
91
- const result = await buildAndPushActivityLogWithPeriod(period, timeFrame, cwd);
92
- console.log(chalk.green('Log saved.'), chalk.dim(result.id));
116
+ try {
117
+ const result = await buildAndPushActivityLogWithPeriod(period, timeFrame, cwd, onProgress);
118
+ spinner.succeed(chalk.green('Log saved.'));
119
+ console.log(chalk.dim(result.id));
120
+ }
121
+ catch (err) {
122
+ spinner.fail(chalk.red(err instanceof Error ? err.message : 'Failed'));
123
+ throw err;
124
+ }
93
125
  return;
94
126
  }
127
+ spinner.stop();
95
128
  const choice = await inquirer.prompt([
96
129
  {
97
130
  name: 'time_frame',
@@ -105,6 +138,14 @@ export const runLog = async (args = []) => {
105
138
  },
106
139
  ]);
107
140
  const timeFrame = choice.time_frame;
108
- const result = await buildAndPushActivityLog(timeFrame, cwd);
109
- console.log(chalk.green('Log saved.'), chalk.dim(result.id));
141
+ spinner.start('Analyzing repository…');
142
+ try {
143
+ const result = await buildAndPushActivityLog(timeFrame, cwd, onProgress);
144
+ spinner.succeed(chalk.green('Log saved.'));
145
+ console.log(chalk.dim(result.id));
146
+ }
147
+ catch (err) {
148
+ spinner.fail(chalk.red(err instanceof Error ? err.message : 'Failed'));
149
+ throw err;
150
+ }
110
151
  };
@@ -83,6 +83,42 @@ const CALLBACK_SUCCESS_HTML = `<!DOCTYPE html>
83
83
  font-size: 0.8125rem;
84
84
  color: ${STYLES.muted};
85
85
  }
86
+ .verify-block {
87
+ margin-top: 1rem;
88
+ padding-top: 1rem;
89
+ border-top: 1px solid ${STYLES.border};
90
+ font-size: 0.8125rem;
91
+ color: ${STYLES.muted};
92
+ }
93
+ .cmd-row {
94
+ display: flex;
95
+ align-items: center;
96
+ justify-content: center;
97
+ gap: 0.5rem;
98
+ margin-top: 0.5rem;
99
+ flex-wrap: wrap;
100
+ }
101
+ .cmd {
102
+ font-family: ui-monospace, monospace;
103
+ font-size: 0.8125rem;
104
+ padding: 0.375rem 0.75rem;
105
+ background: ${STYLES.background};
106
+ border: 1px solid ${STYLES.border};
107
+ border-radius: 0.375rem;
108
+ color: ${STYLES.foreground};
109
+ }
110
+ .copy-btn {
111
+ font-size: 0.8125rem;
112
+ padding: 0.375rem 0.75rem;
113
+ background: ${STYLES.primary};
114
+ color: ${STYLES.primaryForeground};
115
+ border: none;
116
+ border-radius: 0.375rem;
117
+ cursor: pointer;
118
+ font-weight: 500;
119
+ }
120
+ .copy-btn:hover { opacity: 0.9; }
121
+ .copy-btn.copied { background: ${STYLES.muted}; cursor: default; }
86
122
  </style>
87
123
  </head>
88
124
  <body>
@@ -96,7 +132,31 @@ const CALLBACK_SUCCESS_HTML = `<!DOCTYPE html>
96
132
  <h1>You're all set</h1>
97
133
  <p class="muted">You're logged in successfully. You can close this window safely—your credentials are saved and the CLI is ready to use.</p>
98
134
  <p class="hint">Return to your terminal to continue.</p>
135
+ <div class="verify-block">
136
+ <p class="muted" style="margin:0">Verify in terminal:</p>
137
+ <div class="cmd-row">
138
+ <code class="cmd" id="whoami-cmd">bitcompass whoami</code>
139
+ <button type="button" class="copy-btn" id="copy-btn" aria-label="Copy command">Copy</button>
140
+ </div>
141
+ </div>
99
142
  </div>
143
+ <script>
144
+ (function() {
145
+ var btn = document.getElementById('copy-btn');
146
+ var cmd = document.getElementById('whoami-cmd');
147
+ if (!btn || !cmd) return;
148
+ btn.addEventListener('click', function() {
149
+ navigator.clipboard.writeText('bitcompass whoami').then(function() {
150
+ btn.textContent = 'Copied!';
151
+ btn.classList.add('copied');
152
+ setTimeout(function() {
153
+ btn.textContent = 'Copy';
154
+ btn.classList.remove('copied');
155
+ }, 2000);
156
+ });
157
+ });
158
+ })();
159
+ </script>
100
160
  </body>
101
161
  </html>`;
102
162
  const escapeHtml = (s) => s
@@ -1,5 +1,9 @@
1
- export declare const runRulesSearch: (query?: string) => Promise<void>;
2
- export declare const runRulesList: () => Promise<void>;
1
+ export declare const runRulesSearch: (query?: string, options?: {
2
+ listOnly?: boolean;
3
+ }) => Promise<void>;
4
+ export declare const runRulesList: (options?: {
5
+ table?: boolean;
6
+ }) => Promise<void>;
3
7
  export declare const runRulesPull: (id?: string, options?: {
4
8
  global?: boolean;
5
9
  copy?: boolean;
@@ -4,7 +4,8 @@ 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
- export const runRulesSearch = async (query) => {
7
+ import { formatList, shouldUseTable } from '../lib/list-format.js';
8
+ export const runRulesSearch = async (query, options) => {
8
9
  if (!loadCredentials()) {
9
10
  console.error(chalk.red('Not logged in. Run bitcompass login.'));
10
11
  process.exit(1);
@@ -17,6 +18,10 @@ export const runRulesSearch = async (query) => {
17
18
  console.log(chalk.yellow('No rules found.'));
18
19
  return;
19
20
  }
21
+ if (options?.listOnly) {
22
+ formatList(list.map((r) => ({ id: r.id, title: r.title, kind: r.kind })), { useTable: shouldUseTable(), showKind: true });
23
+ return;
24
+ }
20
25
  const choice = await inquirer.prompt([
21
26
  {
22
27
  name: 'id',
@@ -31,7 +36,7 @@ export const runRulesSearch = async (query) => {
31
36
  console.log(rule.body);
32
37
  }
33
38
  };
34
- export const runRulesList = async () => {
39
+ export const runRulesList = async (options) => {
35
40
  if (!loadCredentials()) {
36
41
  console.error(chalk.red('Not logged in. Run bitcompass login.'));
37
42
  process.exit(1);
@@ -39,9 +44,12 @@ export const runRulesList = async () => {
39
44
  const spinner = ora('Loading rules…').start();
40
45
  const list = await fetchRules('rule');
41
46
  spinner.stop();
42
- list.forEach((r) => console.log(`${chalk.cyan(r.title)} ${chalk.dim(r.id)}`));
43
- if (list.length === 0)
47
+ if (list.length === 0) {
44
48
  console.log(chalk.yellow('No rules yet.'));
49
+ return;
50
+ }
51
+ const useTable = shouldUseTable(options?.table);
52
+ formatList(list.map((r) => ({ id: r.id, title: r.title, kind: r.kind })), { useTable, showKind: false });
45
53
  };
46
54
  export const runRulesPull = async (id, options) => {
47
55
  if (!loadCredentials()) {
@@ -82,8 +90,9 @@ export const runRulesPull = async (id, options) => {
82
90
  }
83
91
  catch (error) {
84
92
  spinner.fail(chalk.red('Failed to pull rule'));
85
- console.error(chalk.red(error.message));
86
- process.exit(1);
93
+ const message = error instanceof Error ? error.message : String(error);
94
+ console.error(chalk.red(message));
95
+ process.exit(message.includes('not found') ? 2 : 1);
87
96
  }
88
97
  };
89
98
  export const runRulesPush = async (file) => {
@@ -0,0 +1,11 @@
1
+ export declare const runSkillsSearch: (query?: string, options?: {
2
+ listOnly?: boolean;
3
+ }) => Promise<void>;
4
+ export declare const runSkillsList: (options?: {
5
+ table?: boolean;
6
+ }) => Promise<void>;
7
+ export declare const runSkillsPull: (id?: string, options?: {
8
+ global?: boolean;
9
+ copy?: boolean;
10
+ }) => Promise<void>;
11
+ export declare const runSkillsPush: (file?: string) => Promise<void>;
@@ -0,0 +1,129 @@
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
+ import { formatList, shouldUseTable } from '../lib/list-format.js';
8
+ export const runSkillsSearch = async (query, options) => {
9
+ if (!loadCredentials()) {
10
+ console.error(chalk.red('Not logged in. Run bitcompass login.'));
11
+ process.exit(1);
12
+ }
13
+ const q = query ?? (await inquirer.prompt([{ name: 'q', message: 'Search query', type: 'input' }])).q;
14
+ const spinner = ora('Searching skills…').start();
15
+ const list = await searchRules(q, { kind: 'skill', limit: 20 });
16
+ spinner.stop();
17
+ if (list.length === 0) {
18
+ console.log(chalk.yellow('No skills found.'));
19
+ return;
20
+ }
21
+ if (options?.listOnly) {
22
+ formatList(list.map((r) => ({ id: r.id, title: r.title, kind: r.kind })), { useTable: shouldUseTable(), showKind: true });
23
+ return;
24
+ }
25
+ const choice = await inquirer.prompt([
26
+ {
27
+ name: 'id',
28
+ message: 'Select a skill',
29
+ type: 'list',
30
+ choices: list.map((r) => ({ name: `${r.title} (${r.id})`, value: r.id })),
31
+ },
32
+ ]);
33
+ const rule = await getRuleById(choice.id);
34
+ if (rule) {
35
+ console.log(chalk.cyan(rule.title));
36
+ console.log(rule.body);
37
+ }
38
+ };
39
+ export const runSkillsList = async (options) => {
40
+ if (!loadCredentials()) {
41
+ console.error(chalk.red('Not logged in. Run bitcompass login.'));
42
+ process.exit(1);
43
+ }
44
+ const spinner = ora('Loading skills…').start();
45
+ const list = await fetchRules('skill');
46
+ spinner.stop();
47
+ if (list.length === 0) {
48
+ console.log(chalk.yellow('No skills yet.'));
49
+ return;
50
+ }
51
+ const useTable = shouldUseTable(options?.table);
52
+ formatList(list.map((r) => ({ id: r.id, title: r.title, kind: r.kind })), { useTable, showKind: false });
53
+ };
54
+ export const runSkillsPull = async (id, options) => {
55
+ if (!loadCredentials()) {
56
+ console.error(chalk.red('Not logged in. Run bitcompass login.'));
57
+ process.exit(1);
58
+ }
59
+ let targetId = id;
60
+ if (!targetId) {
61
+ const spinner = ora('Loading skills…').start();
62
+ const list = await fetchRules('skill');
63
+ spinner.stop();
64
+ if (list.length === 0) {
65
+ console.log(chalk.yellow('No skills to pull.'));
66
+ return;
67
+ }
68
+ const choice = await inquirer.prompt([
69
+ { name: 'id', message: 'Select skill', type: 'list', choices: list.map((r) => ({ name: r.title, value: r.id })) },
70
+ ]);
71
+ targetId = choice.id;
72
+ }
73
+ const spinner = ora('Pulling skill…').start();
74
+ try {
75
+ const filename = await pullRuleToFile(targetId, {
76
+ global: options?.global,
77
+ useSymlink: !options?.copy, // Use symlink unless --copy flag is set
78
+ });
79
+ spinner.succeed(chalk.green('Pulled skill'));
80
+ console.log(chalk.dim(filename));
81
+ if (options?.copy) {
82
+ console.log(chalk.dim('Copied as file (not a symlink)'));
83
+ }
84
+ else {
85
+ console.log(chalk.dim('Created symbolic link to cached skill'));
86
+ }
87
+ if (options?.global) {
88
+ console.log(chalk.dim('Installed globally for all projects'));
89
+ }
90
+ }
91
+ catch (error) {
92
+ spinner.fail(chalk.red('Failed to pull skill'));
93
+ const message = error instanceof Error ? error.message : String(error);
94
+ console.error(chalk.red(message));
95
+ process.exit(message.includes('not found') ? 2 : 1);
96
+ }
97
+ };
98
+ export const runSkillsPush = 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 = 'skill';
110
+ }
111
+ catch {
112
+ const lines = raw.split('\n');
113
+ const title = lines[0].replace(/^#\s*/, '') || 'Untitled';
114
+ payload = { kind: 'skill', title, description: '', body: raw };
115
+ }
116
+ }
117
+ else {
118
+ const answers = await inquirer.prompt([
119
+ { name: 'title', message: 'Skill title', type: 'input', default: 'Untitled' },
120
+ { name: 'description', message: 'Description', type: 'input', default: '' },
121
+ { name: 'body', message: 'Skill content', type: 'editor', default: '' },
122
+ ]);
123
+ payload = { kind: 'skill', title: answers.title, description: answers.description, body: answers.body };
124
+ }
125
+ const spinner = ora('Publishing skill…').start();
126
+ const created = await insertRule(payload);
127
+ spinner.succeed(chalk.green('Published skill ') + created.id);
128
+ console.log(chalk.dim(created.title));
129
+ };
@@ -1,4 +1,9 @@
1
- export declare const runSolutionsSearch: (query?: string) => Promise<void>;
1
+ export declare const runSolutionsSearch: (query?: string, options?: {
2
+ listOnly?: boolean;
3
+ }) => Promise<void>;
4
+ export declare const runSolutionsList: (options?: {
5
+ table?: boolean;
6
+ }) => Promise<void>;
2
7
  export declare const runSolutionsPull: (id?: string, options?: {
3
8
  global?: boolean;
4
9
  copy?: boolean;
@@ -4,7 +4,8 @@ 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
- export const runSolutionsSearch = async (query) => {
7
+ import { formatList, shouldUseTable } from '../lib/list-format.js';
8
+ export const runSolutionsSearch = async (query, options) => {
8
9
  if (!loadCredentials()) {
9
10
  console.error(chalk.red('Not logged in. Run bitcompass login.'));
10
11
  process.exit(1);
@@ -17,6 +18,10 @@ export const runSolutionsSearch = async (query) => {
17
18
  console.log(chalk.yellow('No solutions found.'));
18
19
  return;
19
20
  }
21
+ if (options?.listOnly) {
22
+ formatList(list.map((r) => ({ id: r.id, title: r.title, kind: r.kind })), { useTable: shouldUseTable(), showKind: true });
23
+ return;
24
+ }
20
25
  const choice = await inquirer.prompt([
21
26
  {
22
27
  name: 'id',
@@ -31,6 +36,21 @@ export const runSolutionsSearch = async (query) => {
31
36
  console.log(rule.body);
32
37
  }
33
38
  };
39
+ export const runSolutionsList = async (options) => {
40
+ if (!loadCredentials()) {
41
+ console.error(chalk.red('Not logged in. Run bitcompass login.'));
42
+ process.exit(1);
43
+ }
44
+ const spinner = ora('Loading solutions…').start();
45
+ const list = await fetchRules('solution');
46
+ spinner.stop();
47
+ if (list.length === 0) {
48
+ console.log(chalk.yellow('No solutions yet.'));
49
+ return;
50
+ }
51
+ const useTable = shouldUseTable(options?.table);
52
+ formatList(list.map((r) => ({ id: r.id, title: r.title, kind: r.kind })), { useTable, showKind: false });
53
+ };
34
54
  export const runSolutionsPull = async (id, options) => {
35
55
  if (!loadCredentials()) {
36
56
  console.error(chalk.red('Not logged in. Run bitcompass login.'));
@@ -70,8 +90,9 @@ export const runSolutionsPull = async (id, options) => {
70
90
  }
71
91
  catch (error) {
72
92
  spinner.fail(chalk.red('Failed to pull solution'));
73
- console.error(chalk.red(error.message));
74
- process.exit(1);
93
+ const message = error instanceof Error ? error.message : String(error);
94
+ console.error(chalk.red(message));
95
+ process.exit(message.includes('not found') ? 2 : 1);
75
96
  }
76
97
  };
77
98
  export const runSolutionsPush = async (file) => {
package/dist/index.js CHANGED
@@ -10,10 +10,17 @@ import { runInit } from './commands/init.js';
10
10
  import { runLogin } from './commands/login.js';
11
11
  import { runLogout } from './commands/logout.js';
12
12
  import { runMcpStart, runMcpStatus } from './commands/mcp.js';
13
- import { runLog } from './commands/log.js';
13
+ import { runLog, ValidationError } from './commands/log.js';
14
14
  import { runRulesList, runRulesPull, runRulesPush, runRulesSearch } from './commands/rules.js';
15
- import { runSolutionsPull, runSolutionsPush, runSolutionsSearch } from './commands/solutions.js';
15
+ import { runSolutionsList, 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';
18
+ import { runGlossary } from './commands/glossary.js';
16
19
  import { runWhoami } from './commands/whoami.js';
20
+ // Disable chalk colors when NO_COLOR is set or --no-color is passed (must run before any command)
21
+ if (process.env.NO_COLOR !== undefined || process.argv.includes('--no-color')) {
22
+ chalk.level = 0;
23
+ }
17
24
  // Read version from package.json
18
25
  const __dirname = dirname(fileURLToPath(import.meta.url));
19
26
  const packageJsonPath = join(__dirname, '..', 'package.json');
@@ -23,7 +30,8 @@ const program = new Command();
23
30
  program
24
31
  .name('bitcompass')
25
32
  .description('BitCompass CLI - rules, solutions, and MCP server')
26
- .version(version, '-v, -V, --version', 'display version number');
33
+ .version(version, '-v, -V, --version', 'display version number')
34
+ .option('--no-color', 'Disable colored output');
27
35
  program
28
36
  .command('login')
29
37
  .description('Log in with Google (opens browser)')
@@ -36,6 +44,10 @@ program
36
44
  .command('whoami')
37
45
  .description('Show current user (email)')
38
46
  .action(runWhoami);
47
+ program
48
+ .command('glossary')
49
+ .description('Show glossary (rules, solutions, skills, commands)')
50
+ .action(runGlossary);
39
51
  program
40
52
  .command('init')
41
53
  .description('Configure project: editor/AI provider and output folder for rules/docs/commands')
@@ -43,7 +55,19 @@ program
43
55
  program
44
56
  .command('log [dates...]')
45
57
  .description('Collect repo summary and git activity, then push to your activity logs. Optional: bitcompass log YYYY-MM-DD or bitcompass log YYYY-MM-DD YYYY-MM-DD')
46
- .action((dates) => runLog(dates ?? []).catch(handleErr));
58
+ .addHelpText('after', `
59
+ Examples:
60
+ bitcompass log
61
+ bitcompass log 2025-02-01
62
+ bitcompass log 2025-02-01 2025-02-05
63
+ `)
64
+ .action((dates) => runLog(dates ?? []).catch((err) => {
65
+ if (err instanceof ValidationError) {
66
+ console.error(chalk.red(err.message));
67
+ process.exit(2);
68
+ }
69
+ handleErr(err);
70
+ }));
47
71
  const configCmd = program.command('config').description('Show or set config');
48
72
  configCmd.action(runConfigList);
49
73
  configCmd.command('list').description('List config values').action(runConfigList);
@@ -51,25 +75,88 @@ configCmd.command('set <key> <value>').description('Set supabaseUrl, supabaseAno
51
75
  configCmd.command('get <key>').description('Get a config value').action((key) => runConfigGet(key));
52
76
  // rules
53
77
  const rules = program.command('rules').description('Manage rules');
54
- rules.command('search [query]').description('Search rules').action((query) => runRulesSearch(query).catch(handleErr));
55
- rules.command('list').description('List rules').action(() => runRulesList().catch(handleErr));
78
+ rules
79
+ .command('search [query]')
80
+ .description('Search rules')
81
+ .option('-l, --list', 'List results only; do not prompt to select')
82
+ .action((query, cmd) => runRulesSearch(query, { listOnly: cmd?.opts()?.list }).catch(handleErr));
83
+ rules
84
+ .command('list')
85
+ .description('List rules')
86
+ .option('--table', 'Show output in aligned columns (default when TTY)')
87
+ .addHelpText('after', '\nExamples:\n bitcompass rules list\n bitcompass rules list --table\n')
88
+ .action((opts) => runRulesList({ table: opts.table }).catch(handleErr));
56
89
  rules
57
90
  .command('pull [id]')
58
91
  .description('Pull a rule by ID or choose from list (creates symbolic link by default)')
59
92
  .option('-g, --global', 'Install globally to ~/.cursor/rules/ for all projects')
60
93
  .option('--copy', 'Copy file instead of creating symbolic link')
94
+ .addHelpText('after', '\nExamples:\n bitcompass rules pull <id>\n bitcompass rules pull <id> --global\n bitcompass rules pull <id> --copy\n')
61
95
  .action((id, options) => runRulesPull(id, options).catch(handleErr));
62
96
  rules.command('push [file]').description('Push a rule (file or interactive)').action((file) => runRulesPush(file).catch(handleErr));
63
97
  // solutions
64
98
  const solutions = program.command('solutions').description('Manage solutions');
65
- solutions.command('search [query]').description('Search solutions').action((query) => runSolutionsSearch(query).catch(handleErr));
99
+ solutions
100
+ .command('search [query]')
101
+ .description('Search solutions')
102
+ .option('-l, --list', 'List results only; do not prompt to select')
103
+ .action((query, cmd) => runSolutionsSearch(query, { listOnly: cmd?.opts()?.list }).catch(handleErr));
104
+ solutions
105
+ .command('list')
106
+ .description('List solutions')
107
+ .option('--table', 'Show output in aligned columns (default when TTY)')
108
+ .addHelpText('after', '\nExamples:\n bitcompass solutions list\n bitcompass solutions list --table\n')
109
+ .action((opts) => runSolutionsList({ table: opts.table }).catch(handleErr));
66
110
  solutions
67
111
  .command('pull [id]')
68
112
  .description('Pull a solution by ID or choose from list (creates symbolic link by default)')
69
113
  .option('-g, --global', 'Install globally to ~/.cursor/rules/ for all projects')
70
114
  .option('--copy', 'Copy file instead of creating symbolic link')
115
+ .addHelpText('after', '\nExamples:\n bitcompass solutions pull <id>\n bitcompass solutions pull <id> --global\n')
71
116
  .action((id, options) => runSolutionsPull(id, options).catch(handleErr));
72
117
  solutions.command('push [file]').description('Push a solution (file or interactive)').action((file) => runSolutionsPush(file).catch(handleErr));
118
+ // skills
119
+ const skills = program.command('skills').description('Manage skills');
120
+ skills
121
+ .command('search [query]')
122
+ .description('Search skills')
123
+ .option('-l, --list', 'List results only; do not prompt to select')
124
+ .action((query, cmd) => runSkillsSearch(query, { listOnly: cmd?.opts()?.list }).catch(handleErr));
125
+ skills
126
+ .command('list')
127
+ .description('List skills')
128
+ .option('--table', 'Show output in aligned columns (default when TTY)')
129
+ .addHelpText('after', '\nExamples:\n bitcompass skills list\n bitcompass skills list --table\n')
130
+ .action((opts) => runSkillsList({ table: opts.table }).catch(handleErr));
131
+ skills
132
+ .command('pull [id]')
133
+ .description('Pull a skill by ID or choose from list (creates symbolic link by default)')
134
+ .option('-g, --global', 'Install globally to ~/.cursor/rules/ for all projects')
135
+ .option('--copy', 'Copy file instead of creating symbolic link')
136
+ .addHelpText('after', '\nExamples:\n bitcompass skills pull <id>\n bitcompass skills pull <id> --global\n')
137
+ .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));
139
+ // commands
140
+ const commands = program.command('commands').description('Manage commands');
141
+ commands
142
+ .command('search [query]')
143
+ .description('Search commands')
144
+ .option('-l, --list', 'List results only; do not prompt to select')
145
+ .action((query, cmd) => runCommandsSearch(query, { listOnly: cmd?.opts()?.list }).catch(handleErr));
146
+ commands
147
+ .command('list')
148
+ .description('List commands')
149
+ .option('--table', 'Show output in aligned columns (default when TTY)')
150
+ .addHelpText('after', '\nExamples:\n bitcompass commands list\n bitcompass commands list --table\n')
151
+ .action((opts) => runCommandsList({ table: opts.table }).catch(handleErr));
152
+ commands
153
+ .command('pull [id]')
154
+ .description('Pull a command by ID or choose from list (creates symbolic link by default)')
155
+ .option('-g, --global', 'Install globally to ~/.cursor/rules/ for all projects')
156
+ .option('--copy', 'Copy file instead of creating symbolic link')
157
+ .addHelpText('after', '\nExamples:\n bitcompass commands pull <id>\n bitcompass commands pull <id> --global\n')
158
+ .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));
73
160
  // mcp
74
161
  const mcp = program.command('mcp').description('MCP server');
75
162
  mcp.command('start').description('Start MCP server (stdio)').action(() => runMcpStart().catch(handleErr));
@@ -78,4 +165,11 @@ function handleErr(err) {
78
165
  console.error(chalk.red(err instanceof Error ? err.message : String(err)));
79
166
  process.exit(1);
80
167
  }
168
+ if (process.argv.slice(2).length === 0) {
169
+ console.log(chalk.cyan('BitCompass') +
170
+ chalk.dim(' – rules, solutions, and MCP server. Run ') +
171
+ chalk.cyan('bitcompass --help') +
172
+ chalk.dim(' for commands.'));
173
+ process.exit(0);
174
+ }
81
175
  program.parse();
@@ -37,6 +37,11 @@ export declare const getRepoSummary: (repoRoot: string) => RepoSummary;
37
37
  * period_end is now; period_start and since are the start of the window.
38
38
  */
39
39
  export declare const getPeriodForTimeFrame: (timeFrame: TimeFrame) => PeriodBounds;
40
+ /**
41
+ * Parse an ISO date string (YYYY-MM-DD) to Date at start of day (UTC).
42
+ * Returns null for invalid or non-calendar dates (e.g. 2025-02-30).
43
+ */
44
+ export declare const parseDate: (s: string) => Date | null;
40
45
  /**
41
46
  * Compute period for custom date(s). Single date = that day; two dates = range (inclusive).
42
47
  * period_end is end of last day (23:59:59.999).
@@ -74,8 +74,9 @@ export const getPeriodForTimeFrame = (timeFrame) => {
74
74
  };
75
75
  /**
76
76
  * Parse an ISO date string (YYYY-MM-DD) to Date at start of day (UTC).
77
+ * Returns null for invalid or non-calendar dates (e.g. 2025-02-30).
77
78
  */
78
- const parseDate = (s) => {
79
+ export const parseDate = (s) => {
79
80
  const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(s.trim());
80
81
  if (!match)
81
82
  return null;
@@ -94,13 +95,13 @@ const parseDate = (s) => {
94
95
  export const getPeriodForCustomDates = (startDateStr, endDateStr) => {
95
96
  const startDate = parseDate(startDateStr);
96
97
  if (!startDate) {
97
- throw new Error(`Invalid start date: ${startDateStr}. Use YYYY-MM-DD.`);
98
+ throw new Error(`Invalid start date: ${startDateStr}. Use YYYY-MM-DD (e.g. 2025-02-06).`);
98
99
  }
99
100
  let periodEnd;
100
101
  if (endDateStr !== undefined && endDateStr.trim() !== '') {
101
102
  const endDate = parseDate(endDateStr);
102
103
  if (!endDate)
103
- throw new Error(`Invalid end date: ${endDateStr}. Use YYYY-MM-DD.`);
104
+ throw new Error(`Invalid end date: ${endDateStr}. Use YYYY-MM-DD (e.g. 2025-02-06).`);
104
105
  if (endDate < startDate)
105
106
  throw new Error('End date must be on or after start date.');
106
107
  periodEnd = new Date(endDate);
@@ -0,0 +1,15 @@
1
+ export interface ListRow {
2
+ id: string;
3
+ title: string;
4
+ kind?: string;
5
+ }
6
+ /**
7
+ * Format a list of rules/items for terminal output.
8
+ * When useTable is true (TTY or --table), prints aligned columns; otherwise one line per row for scripts/CI.
9
+ */
10
+ export declare const formatList: (list: ListRow[], options: {
11
+ useTable: boolean;
12
+ showKind?: boolean;
13
+ }) => void;
14
+ /** Use table format when stdout is a TTY or when tableFlag is true. */
15
+ export declare const shouldUseTable: (tableFlag?: boolean) => boolean;
@@ -0,0 +1,34 @@
1
+ import chalk from 'chalk';
2
+ const ID_COLUMN_WIDTH = 36;
3
+ const TITLE_MAX_WIDTH = 50;
4
+ const KIND_WIDTH = 10;
5
+ /**
6
+ * Format a list of rules/items for terminal output.
7
+ * When useTable is true (TTY or --table), prints aligned columns; otherwise one line per row for scripts/CI.
8
+ */
9
+ export const formatList = (list, options) => {
10
+ if (list.length === 0)
11
+ return;
12
+ const { useTable, showKind = false } = options;
13
+ if (useTable) {
14
+ const titleWidth = Math.min(TITLE_MAX_WIDTH, Math.max(...list.map((r) => r.title.length), 5));
15
+ const headerKind = showKind ? chalk.dim('Kind'.padEnd(KIND_WIDTH)) + ' ' : '';
16
+ console.log(chalk.bold('ID'.padEnd(ID_COLUMN_WIDTH)) +
17
+ ' ' +
18
+ chalk.bold('Title'.padEnd(titleWidth)) +
19
+ (showKind ? ' ' + chalk.bold('Kind') : ''));
20
+ const sep = '-'.repeat(ID_COLUMN_WIDTH + 1 + titleWidth + (showKind ? KIND_WIDTH + 2 : 0));
21
+ console.log(chalk.dim(sep));
22
+ for (const r of list) {
23
+ const id = r.id.length > ID_COLUMN_WIDTH ? r.id.slice(0, ID_COLUMN_WIDTH - 1) + '…' : r.id.padEnd(ID_COLUMN_WIDTH);
24
+ const title = r.title.length > titleWidth ? r.title.slice(0, titleWidth - 1) + '…' : r.title.padEnd(titleWidth);
25
+ const kindPart = showKind ? ' ' + (r.kind ?? '').padEnd(KIND_WIDTH) : '';
26
+ console.log(chalk.dim(id) + ' ' + chalk.cyan(title) + kindPart);
27
+ }
28
+ }
29
+ else {
30
+ list.forEach((r) => console.log(`${chalk.cyan(r.title)} ${chalk.dim(r.id)}`));
31
+ }
32
+ };
33
+ /** Use table format when stdout is a TTY or when tableFlag is true. */
34
+ export const shouldUseTable = (tableFlag) => Boolean(tableFlag ?? process.stdout.isTTY);
@@ -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
+ };
@@ -1,8 +1,15 @@
1
+ import { readFileSync } from 'fs';
2
+ import { dirname, join } from 'path';
3
+ import { fileURLToPath } from 'url';
1
4
  import { AUTH_REQUIRED_MSG, insertRule, searchRules, getRuleById, fetchRules, updateRule, deleteRule, fetchActivityLogs, getActivityLogById, } from '../api/client.js';
2
5
  import { buildAndPushActivityLog } from '../commands/log.js';
3
6
  import { loadCredentials } from '../auth/config.js';
4
7
  import { getProjectConfig } from '../auth/project-config.js';
5
8
  import { pullRuleToFile } from '../lib/rule-file-ops.js';
9
+ const __dirname = dirname(fileURLToPath(import.meta.url));
10
+ const packageJsonPath = join(__dirname, '..', 'package.json');
11
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
12
+ const VERSION = packageJson.version ?? '0.0.0';
6
13
  /** When token is missing, we fail initialize so Cursor shows "Needs authentication" (yellow) instead of success (green). */
7
14
  const NEEDS_AUTH_ERROR_MESSAGE = 'Needs authentication';
8
15
  const NEEDS_AUTH_ERROR_CODE = -32001; // Server error: auth required
@@ -44,7 +51,7 @@ function createStdioServer() {
44
51
  result: {
45
52
  protocolVersion: '2024-11-05',
46
53
  capabilities: { tools: {}, prompts: {} },
47
- serverInfo: { name: 'bitcompass', version: '0.1.0' },
54
+ serverInfo: { name: 'bitcompass', version: VERSION },
48
55
  },
49
56
  });
50
57
  return;
@@ -57,29 +64,33 @@ function createStdioServer() {
57
64
  tools: [
58
65
  {
59
66
  name: 'search-rules',
60
- description: 'Search BitCompass rules by query',
67
+ description: 'Use when the user wants to find rules, solutions, skills, or commands by keyword or topic. Returns a list of matching items with id, title, kind, author, and a short body snippet. Optionally filter by kind (rule, solution, skill, command) and limit results.',
61
68
  inputSchema: {
62
69
  type: 'object',
63
- properties: { query: { type: 'string' }, kind: { type: 'string', enum: ['rule', 'solution', 'skill', 'command'] }, limit: { type: 'number' } },
70
+ properties: {
71
+ query: { type: 'string', description: 'Search query or keywords' },
72
+ kind: { type: 'string', enum: ['rule', 'solution', 'skill', 'command'], description: 'Optional: restrict to one kind' },
73
+ limit: { type: 'number', description: 'Optional: max results (default 20)' },
74
+ },
64
75
  required: ['query'],
65
76
  },
66
77
  },
67
78
  {
68
79
  name: 'search-solutions',
69
- description: 'Search BitCompass solutions by query',
80
+ description: 'Use when the user asks for problem solutions or how-to guides. Returns a list of solutions with id, title, author, and a short snippet. Prefer this over search-rules when the intent is clearly "solutions" or "problem solutions".',
70
81
  inputSchema: {
71
82
  type: 'object',
72
- properties: { query: { type: 'string' }, limit: { type: 'number' } },
83
+ properties: { query: { type: 'string', description: 'Search query' }, limit: { type: 'number', description: 'Optional: max results' } },
73
84
  required: ['query'],
74
85
  },
75
86
  },
76
87
  {
77
88
  name: 'post-rules',
78
- description: 'Publish a new rule or solution to BitCompass',
89
+ 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).',
79
90
  inputSchema: {
80
91
  type: 'object',
81
92
  properties: {
82
- kind: { type: 'string', enum: ['rule', 'solution', 'skill', 'command'] },
93
+ kind: { type: 'string', enum: ['rule', 'solution', 'skill', 'command'], description: 'Type of entry to publish' },
83
94
  title: { type: 'string' },
84
95
  description: { type: 'string' },
85
96
  body: { type: 'string' },
@@ -92,7 +103,7 @@ function createStdioServer() {
92
103
  },
93
104
  {
94
105
  name: 'create-activity-log',
95
- description: "Collect a summary of the repository and git activity for the chosen period, then push the log to the user's private activity logs. Requires a git repository; if repo_path is not a git repo, returns an error. Ask the user which time frame they want: day, week, or month.",
106
+ description: "Use when the user wants to record their repo activity (commits, files changed) for a time period. Ask for time_frame: day, week, or month. Requires a git repo at repo_path (default: cwd). Returns success and log id, or an error if not a git repo or auth missing.",
96
107
  inputSchema: {
97
108
  type: 'object',
98
109
  properties: {
@@ -104,7 +115,7 @@ function createStdioServer() {
104
115
  },
105
116
  {
106
117
  name: 'get-rule',
107
- description: 'Get full details of a rule or solution by ID',
118
+ description: 'Use when you have a rule/solution ID and need the full content (title, description, body, examples, technologies). Returns the complete rule object or an error if not found. Optional kind filter verifies the entry matches that type.',
108
119
  inputSchema: {
109
120
  type: 'object',
110
121
  properties: {
@@ -116,7 +127,7 @@ function createStdioServer() {
116
127
  },
117
128
  {
118
129
  name: 'list-rules',
119
- description: 'List all rules and solutions (with optional filtering by kind)',
130
+ 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.',
120
131
  inputSchema: {
121
132
  type: 'object',
122
133
  properties: {
@@ -127,7 +138,7 @@ function createStdioServer() {
127
138
  },
128
139
  {
129
140
  name: 'update-rule',
130
- description: 'Update an existing rule or solution',
141
+ 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.',
131
142
  inputSchema: {
132
143
  type: 'object',
133
144
  properties: {
@@ -144,7 +155,7 @@ function createStdioServer() {
144
155
  },
145
156
  {
146
157
  name: 'delete-rule',
147
- description: 'Delete a rule or solution by ID',
158
+ description: 'Use when the user wants to remove a rule or solution by ID. Returns success or error. Requires authentication; user can only delete their own entries.',
148
159
  inputSchema: {
149
160
  type: 'object',
150
161
  properties: {
@@ -155,7 +166,7 @@ function createStdioServer() {
155
166
  },
156
167
  {
157
168
  name: 'pull-rule',
158
- description: 'Pull a rule or solution to a file in the project rules directory',
169
+ description: 'Use when the user wants to install a rule or solution into their project (e.g. "pull this rule to my project"). Writes the rule to the project rules directory or optional output_path; global installs to ~/.cursor/rules/. Returns the file path written or an error.',
159
170
  inputSchema: {
160
171
  type: 'object',
161
172
  properties: {
@@ -168,7 +179,7 @@ function createStdioServer() {
168
179
  },
169
180
  {
170
181
  name: 'list-activity-logs',
171
- description: "List user's activity logs",
182
+ description: "Use when the user wants to see their past activity logs (e.g. 'show my logs', 'list activity'). Optional limit and time_frame (day, week, month). Returns an array of logs with id, time_frame, period_start, period_end, created_at.",
172
183
  inputSchema: {
173
184
  type: 'object',
174
185
  properties: {
@@ -179,7 +190,7 @@ function createStdioServer() {
179
190
  },
180
191
  {
181
192
  name: 'get-activity-log',
182
- description: 'Get activity log details by ID',
193
+ description: 'Use when the user asks for details of a specific activity log by ID. Returns the full log: time_frame, period_start, period_end, repo_summary, git_analysis, created_at.',
183
194
  inputSchema: {
184
195
  type: 'object',
185
196
  properties: {
@@ -299,7 +310,11 @@ function createStdioServer() {
299
310
  const limit = args.limit ?? 20;
300
311
  try {
301
312
  const list = await searchRules(query, { kind, limit });
302
- return { rules: list.map((r) => ({ id: r.id, title: r.title, kind: r.kind, author: r.author_display_name ?? null, snippet: r.body.slice(0, 200) })) };
313
+ const summary = list.length === 0 ? 'No rules found.' : `Found ${list.length} rule(s).`;
314
+ return {
315
+ rules: list.map((r) => ({ id: r.id, title: r.title, kind: r.kind, author: r.author_display_name ?? null, snippet: r.body.slice(0, 200) })),
316
+ summary,
317
+ };
303
318
  }
304
319
  catch (e) {
305
320
  const msg = e instanceof Error ? e.message : 'Search failed.';
@@ -311,7 +326,11 @@ function createStdioServer() {
311
326
  const limit = args.limit ?? 20;
312
327
  try {
313
328
  const list = await searchRules(query, { kind: 'solution', limit });
314
- return { solutions: list.map((r) => ({ id: r.id, title: r.title, author: r.author_display_name ?? null, snippet: r.body.slice(0, 200) })) };
329
+ const summary = list.length === 0 ? 'No solutions found.' : `Found ${list.length} solution(s).`;
330
+ return {
331
+ solutions: list.map((r) => ({ id: r.id, title: r.title, author: r.author_display_name ?? null, snippet: r.body.slice(0, 200) })),
332
+ summary,
333
+ };
315
334
  }
316
335
  catch (e) {
317
336
  const msg = e instanceof Error ? e.message : 'Search failed.';
@@ -395,6 +414,11 @@ function createStdioServer() {
395
414
  try {
396
415
  const list = await fetchRules(kind);
397
416
  const limited = list.slice(0, limit);
417
+ const summary = list.length === 0
418
+ ? 'No rules found.'
419
+ : limited.length < list.length
420
+ ? `Listed ${limited.length} of ${list.length} rule(s).`
421
+ : `Found ${list.length} rule(s).`;
398
422
  return {
399
423
  rules: limited.map((r) => ({
400
424
  id: r.id,
@@ -407,6 +431,7 @@ function createStdioServer() {
407
431
  })),
408
432
  total: list.length,
409
433
  returned: limited.length,
434
+ summary,
410
435
  };
411
436
  }
412
437
  catch (e) {
@@ -486,6 +511,7 @@ function createStdioServer() {
486
511
  const timeFrame = args.time_frame;
487
512
  try {
488
513
  const logs = await fetchActivityLogs({ limit, time_frame: timeFrame });
514
+ const summary = logs.length === 0 ? 'No activity logs found.' : `Found ${logs.length} activity log(s).`;
489
515
  return {
490
516
  logs: logs.map((log) => ({
491
517
  id: log.id,
@@ -495,6 +521,7 @@ function createStdioServer() {
495
521
  created_at: log.created_at,
496
522
  })),
497
523
  total: logs.length,
524
+ summary,
498
525
  };
499
526
  }
500
527
  catch (e) {
package/glossary.md ADDED
@@ -0,0 +1,19 @@
1
+ # BitCompass terminology
2
+
3
+ Shared definitions for rules, solutions, skills, and commands.
4
+
5
+ ## Rule
6
+
7
+ A **rule** is a reusable guideline or standard (e.g. coding style, when to use a tool) that can be pulled into a project. Rules are typically stored in `.cursor/rules/` or similar and guide AI or tooling behavior.
8
+
9
+ ## Solution
10
+
11
+ A **solution** is a step-by-step or narrative answer to a specific problem (e.g. "How to fix X"). Solutions can be pulled like rules and are often used to document fixes or procedures.
12
+
13
+ ## Skill
14
+
15
+ A **skill** is an extended capability or workflow for an agent (e.g. "Figma design-to-code"). Skills are installable into an agent environment and extend what the agent can do.
16
+
17
+ ## Command
18
+
19
+ A **command** is an executable shortcut or script registered in BitCompass. Commands can be discovered and pulled into a project for reuse.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bitcompass",
3
- "version": "0.3.4",
3
+ "version": "0.3.6",
4
4
  "description": "BitCompass CLI - rules, solutions, and MCP server",
5
5
  "type": "module",
6
6
  "bin": {
@@ -49,6 +49,7 @@
49
49
  },
50
50
  "files": [
51
51
  "dist",
52
- "scripts"
52
+ "scripts",
53
+ "glossary.md"
53
54
  ]
54
55
  }