bitcompass 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,90 @@
1
+ # BitCompass CLI
2
+
3
+ CLI for rules, solutions, and MCP server. Same backend as the webapp (Supabase).
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install -g bitcompass
9
+ ```
10
+
11
+ Or run without installing:
12
+
13
+ ```bash
14
+ npx bitcompass --help
15
+ ```
16
+
17
+ Package: [npmjs.com/package/bitcompass](https://www.npmjs.com/package/bitcompass)
18
+
19
+ ## Setup
20
+
21
+ 1. Configure Supabase (required for login and API):
22
+ - `bitcompass config set supabaseUrl https://YOUR_PROJECT.supabase.co`
23
+ - `bitcompass config set supabaseAnonKey YOUR_ANON_KEY`
24
+ - Or set `BITCOMPASS_SUPABASE_URL` and `BITCOMPASS_SUPABASE_ANON_KEY`
25
+ 2. Log in: `bitcompass login` (opens browser)
26
+
27
+ ## Commands
28
+
29
+ - `bitcompass login` – Google login (opens browser)
30
+ - `bitcompass logout` – Remove credentials
31
+ - `bitcompass whoami` – Show current user
32
+ - `bitcompass rules search [query]` – Search rules
33
+ - `bitcompass rules list` – List rules
34
+ - `bitcompass rules pull [id]` – Pull rule to file
35
+ - `bitcompass rules push [file]` – Push rule (or interactive)
36
+ - `bitcompass solutions search|pull|push` – Same for solutions
37
+ - `bitcompass mcp start` – Start MCP server (stdio) for Cursor/IDEs
38
+ - `bitcompass mcp status` – Show MCP login status
39
+ - `bitcompass config` – List config; `config set/get` for values
40
+
41
+ ## MCP
42
+
43
+ ### Cursor (global install)
44
+
45
+ If you installed via `npm install -g bitcompass`, add this to Cursor’s MCP config:
46
+
47
+ **Cursor:** Settings → Features → MCP → Edit config (or open `~/.cursor/mcp.json`).
48
+
49
+ Add the `bitcompass` entry under `mcpServers`:
50
+
51
+ ```json
52
+ {
53
+ "mcpServers": {
54
+ "bitcompass": {
55
+ "type": "stdio",
56
+ "command": "bitcompass",
57
+ "args": ["mcp", "start"],
58
+ "env": {
59
+ "BITCOMPASS_CONFIG_DIR": "~/.bitcompass"
60
+ }
61
+ }
62
+ }
63
+ }
64
+ ```
65
+
66
+ Replace `~` with your full home path if your environment doesn’t expand it. Run `bitcompass login` before using MCP.
67
+
68
+ ### Development (this repo)
69
+
70
+ This repo includes **`.cursor/mcp.json`** so Cursor points at the local CLI when the project is open. Build and log in:
71
+
72
+ ```bash
73
+ cd packages/bitcompass-cli && npm run build && bitcompass login
74
+ ```
75
+
76
+ **Manual (local path):** Settings → MCP → stdio, Command **node**, Args **path/to/packages/bitcompass-cli/dist/index.js** **mcp** **start**. Optionally set env or envFile to the CLI `.env`.
77
+
78
+ Tools: `search-rules`, `search-solutions`, `post-rules`. Prompts: `share_new_rule`, `share_problem_solution`.
79
+
80
+ ## Publish (maintainers)
81
+
82
+ From the CLI package directory:
83
+
84
+ ```bash
85
+ cd packages/bitcompass-cli
86
+ npm run build
87
+ npm publish
88
+ ```
89
+
90
+ For a scoped package use `npm publish --access public`.
@@ -0,0 +1,12 @@
1
+ import { type SupabaseClient } from '@supabase/supabase-js';
2
+ import type { Rule, RuleInsert } from '../types.js';
3
+ export declare const getSupabaseClient: () => SupabaseClient | null;
4
+ export declare const fetchRules: (kind?: "rule" | "solution") => Promise<Rule[]>;
5
+ export declare const searchRules: (queryText: string, options?: {
6
+ kind?: "rule" | "solution";
7
+ limit?: number;
8
+ }) => Promise<Rule[]>;
9
+ export declare const getRuleById: (id: string) => Promise<Rule | null>;
10
+ export declare const insertRule: (rule: RuleInsert) => Promise<Rule>;
11
+ export declare const updateRule: (id: string, updates: Partial<RuleInsert>) => Promise<Rule>;
12
+ export declare const deleteRule: (id: string) => Promise<void>;
@@ -0,0 +1,89 @@
1
+ import { createClient } from '@supabase/supabase-js';
2
+ import { loadConfig, loadCredentials } from '../auth/config.js';
3
+ export const getSupabaseClient = () => {
4
+ const config = loadConfig();
5
+ const creds = loadCredentials();
6
+ const url = config.supabaseUrl ?? process.env.BITCOMPASS_SUPABASE_URL;
7
+ const key = config.supabaseAnonKey ?? process.env.BITCOMPASS_SUPABASE_ANON_KEY;
8
+ if (!url || !key) {
9
+ return null;
10
+ }
11
+ const accessToken = creds?.access_token;
12
+ const client = createClient(url, key, {
13
+ global: accessToken
14
+ ? { headers: { Authorization: `Bearer ${accessToken}` } }
15
+ : undefined,
16
+ });
17
+ return client;
18
+ };
19
+ export const fetchRules = async (kind) => {
20
+ const client = getSupabaseClient();
21
+ if (!client) {
22
+ throw new Error('Supabase not configured. Set BITCOMPASS_SUPABASE_URL and BITCOMPASS_SUPABASE_ANON_KEY or run bitcompass config.');
23
+ }
24
+ let query = client.from('rules').select('*').order('created_at', { ascending: false });
25
+ if (kind) {
26
+ query = query.eq('kind', kind);
27
+ }
28
+ const { data, error } = await query;
29
+ if (error)
30
+ throw new Error(error.message);
31
+ return (data ?? []);
32
+ };
33
+ export const searchRules = async (queryText, options = {}) => {
34
+ const client = getSupabaseClient();
35
+ if (!client) {
36
+ throw new Error('Supabase not configured.');
37
+ }
38
+ let query = client
39
+ .from('rules')
40
+ .select('*')
41
+ .or(`title.ilike.%${queryText}%,description.ilike.%${queryText}%,body.ilike.%${queryText}%`)
42
+ .order('created_at', { ascending: false })
43
+ .limit(options.limit ?? 20);
44
+ if (options.kind) {
45
+ query = query.eq('kind', options.kind);
46
+ }
47
+ const { data, error } = await query;
48
+ if (error)
49
+ throw new Error(error.message);
50
+ return (data ?? []);
51
+ };
52
+ export const getRuleById = async (id) => {
53
+ const client = getSupabaseClient();
54
+ if (!client)
55
+ throw new Error('Supabase not configured.');
56
+ const { data, error } = await client.from('rules').select('*').eq('id', id).single();
57
+ if (error) {
58
+ if (error.code === 'PGRST116')
59
+ return null;
60
+ throw new Error(error.message);
61
+ }
62
+ return data;
63
+ };
64
+ export const insertRule = async (rule) => {
65
+ const client = getSupabaseClient();
66
+ if (!client)
67
+ throw new Error('Supabase not configured.');
68
+ const { data, error } = await client.from('rules').insert(rule).select().single();
69
+ if (error)
70
+ throw new Error(error.message);
71
+ return data;
72
+ };
73
+ export const updateRule = async (id, updates) => {
74
+ const client = getSupabaseClient();
75
+ if (!client)
76
+ throw new Error('Supabase not configured.');
77
+ const { data, error } = await client.from('rules').update(updates).eq('id', id).select().single();
78
+ if (error)
79
+ throw new Error(error.message);
80
+ return data;
81
+ };
82
+ export const deleteRule = async (id) => {
83
+ const client = getSupabaseClient();
84
+ if (!client)
85
+ throw new Error('Supabase not configured.');
86
+ const { error } = await client.from('rules').delete().eq('id', id);
87
+ if (error)
88
+ throw new Error(error.message);
89
+ };
@@ -0,0 +1,9 @@
1
+ import type { BitcompassConfig, StoredCredentials } from '../types.js';
2
+ export declare const getConfigDir: () => string;
3
+ export declare const getTokenFilePath: () => string;
4
+ export declare const loadConfig: () => BitcompassConfig;
5
+ export declare const saveConfig: (config: BitcompassConfig) => void;
6
+ export declare const loadCredentials: () => StoredCredentials | null;
7
+ export declare const saveCredentials: (creds: StoredCredentials) => void;
8
+ export declare const clearCredentials: () => void;
9
+ export declare const isLoggedIn: () => boolean;
@@ -0,0 +1,81 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
2
+ import { homedir } from 'os';
3
+ import { join } from 'path';
4
+ const getDir = () => {
5
+ const base = process.env.BITCOMPASS_CONFIG_DIR ?? join(homedir(), '.bitcompass');
6
+ return base;
7
+ };
8
+ let _dir = null;
9
+ const getDirCached = () => {
10
+ if (_dir === null)
11
+ _dir = getDir();
12
+ return _dir;
13
+ };
14
+ const CONFIG_FILE = () => join(getDirCached(), 'config.json');
15
+ const TOKEN_FILE = () => join(getDirCached(), 'token.json');
16
+ const ensureDir = () => {
17
+ const dir = getDirCached();
18
+ if (!existsSync(dir)) {
19
+ mkdirSync(dir, { mode: 0o700, recursive: true });
20
+ }
21
+ };
22
+ export const getConfigDir = () => {
23
+ ensureDir();
24
+ return getDirCached();
25
+ };
26
+ export const getTokenFilePath = () => TOKEN_FILE();
27
+ export const loadConfig = () => {
28
+ ensureDir();
29
+ const path = CONFIG_FILE();
30
+ if (!existsSync(path)) {
31
+ return {};
32
+ }
33
+ try {
34
+ const raw = readFileSync(path, 'utf-8');
35
+ return JSON.parse(raw);
36
+ }
37
+ catch {
38
+ return {};
39
+ }
40
+ };
41
+ export const saveConfig = (config) => {
42
+ ensureDir();
43
+ writeFileSync(CONFIG_FILE(), JSON.stringify(config, null, 2), { mode: 0o600 });
44
+ };
45
+ export const loadCredentials = () => {
46
+ ensureDir();
47
+ const path = TOKEN_FILE();
48
+ if (!existsSync(path)) {
49
+ return null;
50
+ }
51
+ try {
52
+ const raw = readFileSync(path, 'utf-8');
53
+ return JSON.parse(raw);
54
+ }
55
+ catch {
56
+ return null;
57
+ }
58
+ };
59
+ export const saveCredentials = (creds) => {
60
+ ensureDir();
61
+ const path = TOKEN_FILE();
62
+ writeFileSync(path, JSON.stringify(creds, null, 0), { mode: 0o600 });
63
+ if (!existsSync(path)) {
64
+ throw new Error(`Token file was not created at ${path}`);
65
+ }
66
+ };
67
+ export const clearCredentials = () => {
68
+ const path = TOKEN_FILE();
69
+ if (existsSync(path)) {
70
+ try {
71
+ writeFileSync(path, '{}', { mode: 0o600 });
72
+ }
73
+ catch {
74
+ // ignore
75
+ }
76
+ }
77
+ };
78
+ export const isLoggedIn = () => {
79
+ const creds = loadCredentials();
80
+ return Boolean(creds?.access_token);
81
+ };
@@ -0,0 +1,3 @@
1
+ export declare const runConfigList: () => void;
2
+ export declare const runConfigSet: (key: string, value: string) => void;
3
+ export declare const runConfigGet: (key: string) => void;
@@ -0,0 +1,33 @@
1
+ import { loadConfig, saveConfig, getConfigDir } from '../auth/config.js';
2
+ import chalk from 'chalk';
3
+ const CONFIG_KEYS = ['supabaseUrl', 'supabaseAnonKey', 'apiUrl'];
4
+ export const runConfigList = () => {
5
+ const dir = getConfigDir();
6
+ const config = loadConfig();
7
+ console.log(chalk.dim('Config dir:'), dir);
8
+ console.log('');
9
+ CONFIG_KEYS.forEach((key) => {
10
+ const val = config[key] ?? process.env[`BITCOMPASS_${key.toUpperCase()}`] ?? '(not set)';
11
+ const display = typeof val === 'string' && val.length > 40 ? val.slice(0, 40) + '…' : val;
12
+ console.log(` ${key}: ${display}`);
13
+ });
14
+ };
15
+ export const runConfigSet = (key, value) => {
16
+ const config = loadConfig();
17
+ if (!CONFIG_KEYS.includes(key)) {
18
+ console.error(chalk.red('Unknown key. Use one of:'), CONFIG_KEYS.join(', '));
19
+ process.exit(1);
20
+ }
21
+ config[key] = value;
22
+ saveConfig(config);
23
+ console.log(chalk.green('Updated'), key);
24
+ };
25
+ export const runConfigGet = (key) => {
26
+ const config = loadConfig();
27
+ if (!CONFIG_KEYS.includes(key)) {
28
+ console.error(chalk.red('Unknown key.'));
29
+ process.exit(1);
30
+ }
31
+ const val = config[key] ?? process.env[`BITCOMPASS_${key.toUpperCase()}`];
32
+ console.log(val ?? '');
33
+ };
@@ -0,0 +1 @@
1
+ export declare const runLogin: () => Promise<void>;
@@ -0,0 +1,146 @@
1
+ import { createClient } from '@supabase/supabase-js';
2
+ import chalk from 'chalk';
3
+ import { createServer } from 'http';
4
+ import open from 'open';
5
+ import ora from 'ora';
6
+ import { getTokenFilePath, loadConfig, saveCredentials } from '../auth/config.js';
7
+ const CALLBACK_PORT = 38473;
8
+ const createInMemoryStorage = () => {
9
+ const store = new Map();
10
+ return {
11
+ getItem: async (key) => store.get(key) ?? null,
12
+ setItem: async (key, value) => {
13
+ store.set(key, value);
14
+ },
15
+ removeItem: async (key) => {
16
+ store.delete(key);
17
+ },
18
+ };
19
+ };
20
+ export const runLogin = async () => {
21
+ const config = loadConfig();
22
+ const url = config.supabaseUrl ?? process.env.BITCOMPASS_SUPABASE_URL;
23
+ const anonKey = config.supabaseAnonKey ?? process.env.BITCOMPASS_SUPABASE_ANON_KEY;
24
+ if (!url || !anonKey) {
25
+ console.error(chalk.red('Supabase not configured. Set supabaseUrl and supabaseAnonKey:\n bitcompass config set supabaseUrl https://YOUR_PROJECT.supabase.co\n bitcompass config set supabaseAnonKey YOUR_ANON_KEY\nOr set BITCOMPASS_SUPABASE_URL and BITCOMPASS_SUPABASE_ANON_KEY.'));
26
+ process.exit(1);
27
+ }
28
+ const redirectTo = `http://127.0.0.1:${CALLBACK_PORT}/callback`;
29
+ const storage = createInMemoryStorage();
30
+ const supabase = createClient(url, anonKey, {
31
+ auth: {
32
+ flowType: 'pkce',
33
+ storage,
34
+ detectSessionInUrl: false,
35
+ },
36
+ });
37
+ const spinner = ora('Opening browser for Google login…').start();
38
+ return new Promise((resolve, reject) => {
39
+ const server = createServer(async (req, res) => {
40
+ const u = new URL(req.url ?? '/', `http://127.0.0.1:${CALLBACK_PORT}`);
41
+ if (u.pathname === '/callback') {
42
+ const code = u.searchParams.get('code');
43
+ const errorDesc = u.searchParams.get('error_description');
44
+ if (errorDesc) {
45
+ res.writeHead(200, { 'Content-Type': 'text/html' });
46
+ res.end(`<!DOCTYPE html><html><body><p>Login failed: ${errorDesc}</p></body></html>`);
47
+ spinner.fail(chalk.red('Login failed: ' + errorDesc));
48
+ server.close();
49
+ reject(new Error(errorDesc));
50
+ return;
51
+ }
52
+ if (!code) {
53
+ res.writeHead(200, { 'Content-Type': 'text/html' });
54
+ res.end('<!DOCTYPE html><html><body><p>No authorization code in URL. Try again.</p></body></html>');
55
+ spinner.fail(chalk.red('No code received. Try again.'));
56
+ server.close();
57
+ reject(new Error('No code'));
58
+ return;
59
+ }
60
+ try {
61
+ const { data, error } = await supabase.auth.exchangeCodeForSession(code);
62
+ if (error) {
63
+ res.writeHead(200, { 'Content-Type': 'text/html' });
64
+ res.end(`<!DOCTYPE html><html><body><p>Exchange failed: ${error.message}</p></body></html>`);
65
+ spinner.fail(chalk.red('Login failed: ' + error.message));
66
+ server.close();
67
+ reject(error);
68
+ return;
69
+ }
70
+ const session = data?.session;
71
+ if (!session?.access_token) {
72
+ res.writeHead(200, { 'Content-Type': 'text/html' });
73
+ res.end('<!DOCTYPE html><html><body><p>No session received.</p></body></html>');
74
+ spinner.fail(chalk.red('No session.'));
75
+ server.close();
76
+ reject(new Error('No session'));
77
+ return;
78
+ }
79
+ const creds = {
80
+ access_token: session.access_token,
81
+ refresh_token: session.refresh_token ?? '',
82
+ user: session.user ? { email: session.user.email ?? undefined } : {},
83
+ };
84
+ const tokenPath = getTokenFilePath();
85
+ saveCredentials(creds);
86
+ res.writeHead(200, { 'Content-Type': 'text/html' });
87
+ res.end('<!DOCTYPE html><html><body><p>Logged in. You can close this window.</p></body></html>');
88
+ spinner.succeed(chalk.green('Logged in successfully.'));
89
+ console.log(chalk.dim('Credentials saved to:'), tokenPath);
90
+ server.close();
91
+ resolve();
92
+ }
93
+ catch (e) {
94
+ res.writeHead(200, { 'Content-Type': 'text/html' });
95
+ res.end(`<!DOCTYPE html><html><body><p>Error: ${e instanceof Error ? e.message : String(e)}</p></body></html>`);
96
+ spinner.fail('Login failed.');
97
+ console.error(chalk.red(e instanceof Error ? e.message : String(e)));
98
+ server.close();
99
+ reject(e);
100
+ }
101
+ return;
102
+ }
103
+ res.writeHead(404);
104
+ res.end();
105
+ });
106
+ server.listen(CALLBACK_PORT, '127.0.0.1', async () => {
107
+ spinner.text = 'Waiting for login…';
108
+ try {
109
+ const { data, error } = await supabase.auth.signInWithOAuth({
110
+ provider: 'google',
111
+ options: {
112
+ redirectTo,
113
+ scopes: 'openid email profile',
114
+ queryParams: { access_type: 'offline', prompt: 'consent' },
115
+ skipBrowserRedirect: true,
116
+ },
117
+ });
118
+ if (error) {
119
+ spinner.fail(chalk.red('Login failed: ' + error.message));
120
+ server.close();
121
+ reject(error);
122
+ return;
123
+ }
124
+ const authUrl = data?.url;
125
+ if (!authUrl) {
126
+ spinner.fail(chalk.red('No auth URL returned.'));
127
+ server.close();
128
+ reject(new Error('No auth URL'));
129
+ return;
130
+ }
131
+ open(authUrl).catch(() => {
132
+ console.log(chalk.yellow('Open this URL in your browser:'), authUrl);
133
+ });
134
+ }
135
+ catch (e) {
136
+ spinner.fail('Login failed.');
137
+ server.close();
138
+ reject(e);
139
+ }
140
+ });
141
+ server.on('error', (err) => {
142
+ spinner.fail('Could not start callback server.');
143
+ reject(err);
144
+ });
145
+ });
146
+ };
@@ -0,0 +1 @@
1
+ export declare const runLogout: () => void;
@@ -0,0 +1,6 @@
1
+ import { clearCredentials } from '../auth/config.js';
2
+ import chalk from 'chalk';
3
+ export const runLogout = () => {
4
+ clearCredentials();
5
+ console.log(chalk.green('Logged out.'));
6
+ };
@@ -0,0 +1,2 @@
1
+ export declare const runMcpStart: () => Promise<void>;
2
+ export declare const runMcpStatus: () => void;
@@ -0,0 +1,18 @@
1
+ import chalk from 'chalk';
2
+ import { isLoggedIn } from '../auth/config.js';
3
+ import { startMcpServer } from '../mcp/server.js';
4
+ export const runMcpStart = async () => {
5
+ if (!isLoggedIn()) {
6
+ console.error(chalk.red('Not logged in. Run bitcompass login first.'));
7
+ process.exit(1);
8
+ }
9
+ await startMcpServer();
10
+ };
11
+ export const runMcpStatus = () => {
12
+ if (isLoggedIn()) {
13
+ console.log(chalk.green('MCP: ready (logged in)'));
14
+ }
15
+ else {
16
+ console.log(chalk.yellow('MCP: not logged in. Run bitcompass login.'));
17
+ }
18
+ };
@@ -0,0 +1,4 @@
1
+ export declare const runRulesSearch: (query?: string) => Promise<void>;
2
+ export declare const runRulesList: () => Promise<void>;
3
+ export declare const runRulesPull: (id?: string) => Promise<void>;
4
+ export declare const runRulesPush: (file?: string) => Promise<void>;
@@ -0,0 +1,105 @@
1
+ import inquirer from 'inquirer';
2
+ import ora from 'ora';
3
+ import chalk from 'chalk';
4
+ import { writeFileSync } from 'fs';
5
+ import { loadCredentials } from '../auth/config.js';
6
+ import { searchRules, fetchRules, getRuleById, insertRule } from '../api/client.js';
7
+ export const runRulesSearch = 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 rules…').start();
14
+ const list = await searchRules(q, { kind: 'rule', limit: 20 });
15
+ spinner.stop();
16
+ if (list.length === 0) {
17
+ console.log(chalk.yellow('No rules found.'));
18
+ return;
19
+ }
20
+ const choice = await inquirer.prompt([
21
+ {
22
+ name: 'id',
23
+ message: 'Select a rule',
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 runRulesList = 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 rules…').start();
40
+ const list = await fetchRules('rule');
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 rules yet.'));
45
+ };
46
+ export const runRulesPull = async (id) => {
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 rules…').start();
54
+ const list = await fetchRules('rule');
55
+ spinner.stop();
56
+ if (list.length === 0) {
57
+ console.log(chalk.yellow('No rules to pull.'));
58
+ return;
59
+ }
60
+ const choice = await inquirer.prompt([
61
+ { name: 'id', message: 'Select rule', type: 'list', choices: list.map((r) => ({ name: r.title, value: r.id })) },
62
+ ]);
63
+ targetId = choice.id;
64
+ }
65
+ const rule = await getRuleById(targetId);
66
+ if (!rule) {
67
+ console.error(chalk.red('Rule not found.'));
68
+ process.exit(1);
69
+ }
70
+ const filename = `rule-${rule.id}.md`;
71
+ const content = `# ${rule.title}\n\n${rule.description}\n\n${rule.body}\n`;
72
+ writeFileSync(filename, content);
73
+ console.log(chalk.green('Wrote'), filename);
74
+ };
75
+ export const runRulesPush = async (file) => {
76
+ if (!loadCredentials()) {
77
+ console.error(chalk.red('Not logged in. Run bitcompass login.'));
78
+ process.exit(1);
79
+ }
80
+ let payload;
81
+ if (file) {
82
+ const { readFileSync } = await import('fs');
83
+ const raw = readFileSync(file, 'utf-8');
84
+ try {
85
+ payload = JSON.parse(raw);
86
+ }
87
+ catch {
88
+ const lines = raw.split('\n');
89
+ const title = lines[0].replace(/^#\s*/, '') || 'Untitled';
90
+ payload = { kind: 'rule', title, description: '', body: raw };
91
+ }
92
+ }
93
+ else {
94
+ const answers = await inquirer.prompt([
95
+ { name: 'title', message: 'Rule title', type: 'input', default: 'Untitled' },
96
+ { name: 'description', message: 'Description', type: 'input', default: '' },
97
+ { name: 'body', message: 'Rule content', type: 'editor', default: '' },
98
+ ]);
99
+ payload = { kind: 'rule', title: answers.title, description: answers.description, body: answers.body };
100
+ }
101
+ const spinner = ora('Publishing rule…').start();
102
+ const created = await insertRule(payload);
103
+ spinner.succeed(chalk.green('Published rule ') + created.id);
104
+ console.log(chalk.dim(created.title));
105
+ };
@@ -0,0 +1,3 @@
1
+ export declare const runSolutionsSearch: (query?: string) => Promise<void>;
2
+ export declare const runSolutionsPull: (id?: string) => Promise<void>;
3
+ export declare const runSolutionsPush: (file?: string) => Promise<void>;
@@ -0,0 +1,94 @@
1
+ import inquirer from 'inquirer';
2
+ import ora from 'ora';
3
+ import chalk from 'chalk';
4
+ import { writeFileSync } from 'fs';
5
+ import { loadCredentials } from '../auth/config.js';
6
+ import { searchRules, fetchRules, getRuleById, insertRule } from '../api/client.js';
7
+ export const runSolutionsSearch = 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 solutions…').start();
14
+ const list = await searchRules(q, { kind: 'solution', limit: 20 });
15
+ spinner.stop();
16
+ if (list.length === 0) {
17
+ console.log(chalk.yellow('No solutions found.'));
18
+ return;
19
+ }
20
+ const choice = await inquirer.prompt([
21
+ {
22
+ name: 'id',
23
+ message: 'Select a solution',
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 runSolutionsPull = async (id) => {
35
+ if (!loadCredentials()) {
36
+ console.error(chalk.red('Not logged in. Run bitcompass login.'));
37
+ process.exit(1);
38
+ }
39
+ let targetId = id;
40
+ if (!targetId) {
41
+ const spinner = ora('Loading solutions…').start();
42
+ const list = await fetchRules('solution');
43
+ spinner.stop();
44
+ if (list.length === 0) {
45
+ console.log(chalk.yellow('No solutions to pull.'));
46
+ return;
47
+ }
48
+ const choice = await inquirer.prompt([
49
+ { name: 'id', message: 'Select solution', type: 'list', choices: list.map((r) => ({ name: r.title, value: r.id })) },
50
+ ]);
51
+ targetId = choice.id;
52
+ }
53
+ const rule = await getRuleById(targetId);
54
+ if (!rule) {
55
+ console.error(chalk.red('Solution not found.'));
56
+ process.exit(1);
57
+ }
58
+ const filename = `solution-${rule.id}.md`;
59
+ const content = `# ${rule.title}\n\n${rule.description}\n\n## Solution\n\n${rule.body}\n`;
60
+ writeFileSync(filename, content);
61
+ console.log(chalk.green('Wrote'), filename);
62
+ };
63
+ export const runSolutionsPush = async (file) => {
64
+ if (!loadCredentials()) {
65
+ console.error(chalk.red('Not logged in. Run bitcompass login.'));
66
+ process.exit(1);
67
+ }
68
+ let payload;
69
+ if (file) {
70
+ const { readFileSync } = await import('fs');
71
+ const raw = readFileSync(file, 'utf-8');
72
+ try {
73
+ payload = JSON.parse(raw);
74
+ payload.kind = 'solution';
75
+ }
76
+ catch {
77
+ const lines = raw.split('\n');
78
+ const title = lines[0].replace(/^#\s*/, '') || 'Untitled';
79
+ payload = { kind: 'solution', title, description: '', body: raw };
80
+ }
81
+ }
82
+ else {
83
+ const answers = await inquirer.prompt([
84
+ { name: 'title', message: 'Problem title', type: 'input', default: 'Untitled' },
85
+ { name: 'description', message: 'Description', type: 'input', default: '' },
86
+ { name: 'body', message: 'Solution content', type: 'editor', default: '' },
87
+ ]);
88
+ payload = { kind: 'solution', title: answers.title, description: answers.description, body: answers.body };
89
+ }
90
+ const spinner = ora('Publishing solution…').start();
91
+ const created = await insertRule(payload);
92
+ spinner.succeed(chalk.green('Published solution ') + created.id);
93
+ console.log(chalk.dim(created.title));
94
+ };
@@ -0,0 +1 @@
1
+ export declare const runWhoami: () => void;
@@ -0,0 +1,16 @@
1
+ import { loadCredentials } from '../auth/config.js';
2
+ import chalk from 'chalk';
3
+ export const runWhoami = () => {
4
+ const creds = loadCredentials();
5
+ if (!creds?.access_token) {
6
+ console.error(chalk.red('Not logged in. Run bitcompass login.'));
7
+ process.exit(1);
8
+ }
9
+ const email = creds.user?.email;
10
+ if (email) {
11
+ console.log(email);
12
+ }
13
+ else {
14
+ console.log(chalk.yellow('Logged in (email not stored). Run bitcompass login to refresh.'));
15
+ }
16
+ };
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import 'dotenv/config';
package/dist/index.js ADDED
@@ -0,0 +1,53 @@
1
+ #!/usr/bin/env node
2
+ import 'dotenv/config';
3
+ import chalk from 'chalk';
4
+ import { Command } from 'commander';
5
+ import { runConfigGet, runConfigList, runConfigSet } from './commands/config-cmd.js';
6
+ import { runLogin } from './commands/login.js';
7
+ import { runLogout } from './commands/logout.js';
8
+ import { runMcpStart, runMcpStatus } from './commands/mcp.js';
9
+ import { runRulesList, runRulesPull, runRulesPush, runRulesSearch } from './commands/rules.js';
10
+ import { runSolutionsPull, runSolutionsPush, runSolutionsSearch } from './commands/solutions.js';
11
+ import { runWhoami } from './commands/whoami.js';
12
+ const program = new Command();
13
+ program
14
+ .name('bitcompass')
15
+ .description('BitCompass CLI - rules, solutions, and MCP server')
16
+ .version('0.1.0');
17
+ program
18
+ .command('login')
19
+ .description('Log in with Google (opens browser)')
20
+ .action(() => runLogin().catch((err) => { console.error(chalk.red(err.message)); process.exit(1); }));
21
+ program
22
+ .command('logout')
23
+ .description('Remove stored credentials')
24
+ .action(runLogout);
25
+ program
26
+ .command('whoami')
27
+ .description('Show current user (email)')
28
+ .action(runWhoami);
29
+ const configCmd = program.command('config').description('Show or set config');
30
+ configCmd.action(runConfigList);
31
+ configCmd.command('list').description('List config values').action(runConfigList);
32
+ configCmd.command('set <key> <value>').description('Set supabaseUrl, supabaseAnonKey, or apiUrl').action((key, value) => runConfigSet(key, value));
33
+ configCmd.command('get <key>').description('Get a config value').action((key) => runConfigGet(key));
34
+ // rules
35
+ const rules = program.command('rules').description('Manage rules');
36
+ rules.command('search [query]').description('Search rules').action((query) => runRulesSearch(query).catch(handleErr));
37
+ rules.command('list').description('List rules').action(() => runRulesList().catch(handleErr));
38
+ rules.command('pull [id]').description('Pull a rule by ID or choose from list').action((id) => runRulesPull(id).catch(handleErr));
39
+ rules.command('push [file]').description('Push a rule (file or interactive)').action((file) => runRulesPush(file).catch(handleErr));
40
+ // solutions
41
+ const solutions = program.command('solutions').description('Manage solutions');
42
+ solutions.command('search [query]').description('Search solutions').action((query) => runSolutionsSearch(query).catch(handleErr));
43
+ solutions.command('pull [id]').description('Pull a solution by ID or choose from list').action((id) => runSolutionsPull(id).catch(handleErr));
44
+ solutions.command('push [file]').description('Push a solution (file or interactive)').action((file) => runSolutionsPush(file).catch(handleErr));
45
+ // mcp
46
+ const mcp = program.command('mcp').description('MCP server');
47
+ mcp.command('start').description('Start MCP server (stdio)').action(() => runMcpStart().catch(handleErr));
48
+ mcp.command('status').description('Show MCP status').action(runMcpStatus);
49
+ function handleErr(err) {
50
+ console.error(chalk.red(err instanceof Error ? err.message : String(err)));
51
+ process.exit(1);
52
+ }
53
+ program.parse();
@@ -0,0 +1 @@
1
+ export declare const startMcpServer: () => Promise<void>;
@@ -0,0 +1,225 @@
1
+ import { insertRule, searchRules } from '../api/client.js';
2
+ import { loadCredentials } from '../auth/config.js';
3
+ function createStdioServer() {
4
+ const handlers = new Map();
5
+ let requestId = 0;
6
+ const send = (msg) => {
7
+ process.stdout.write(JSON.stringify(msg) + '\n');
8
+ };
9
+ const handleRequest = async (msg) => {
10
+ const isNotification = msg.id === undefined || msg.id === null;
11
+ const id = isNotification ? undefined : msg.id;
12
+ try {
13
+ if (msg.method === 'notified' && msg.params?.method === 'notifications/initialized') {
14
+ return;
15
+ }
16
+ if (isNotification)
17
+ return;
18
+ if (msg.method === 'initialize') {
19
+ send({
20
+ jsonrpc: '2.0',
21
+ id,
22
+ result: {
23
+ protocolVersion: '2024-11-05',
24
+ capabilities: { tools: {}, prompts: {} },
25
+ serverInfo: { name: 'bitcompass', version: '0.1.0' },
26
+ },
27
+ });
28
+ return;
29
+ }
30
+ if (msg.method === 'tools/list') {
31
+ send({
32
+ jsonrpc: '2.0',
33
+ id,
34
+ result: {
35
+ tools: [
36
+ {
37
+ name: 'search-rules',
38
+ description: 'Search BitCompass rules by query',
39
+ inputSchema: {
40
+ type: 'object',
41
+ properties: { query: { type: 'string' }, kind: { type: 'string', enum: ['rule', 'solution'] }, limit: { type: 'number' } },
42
+ required: ['query'],
43
+ },
44
+ },
45
+ {
46
+ name: 'search-solutions',
47
+ description: 'Search BitCompass solutions by query',
48
+ inputSchema: {
49
+ type: 'object',
50
+ properties: { query: { type: 'string' }, limit: { type: 'number' } },
51
+ required: ['query'],
52
+ },
53
+ },
54
+ {
55
+ name: 'post-rules',
56
+ description: 'Publish a new rule or solution to BitCompass',
57
+ inputSchema: {
58
+ type: 'object',
59
+ properties: {
60
+ kind: { type: 'string', enum: ['rule', 'solution'] },
61
+ title: { type: 'string' },
62
+ description: { type: 'string' },
63
+ body: { type: 'string' },
64
+ context: { type: 'string' },
65
+ examples: { type: 'array', items: { type: 'string' } },
66
+ technologies: { type: 'array', items: { type: 'string' } },
67
+ },
68
+ required: ['kind', 'title', 'body'],
69
+ },
70
+ },
71
+ ],
72
+ },
73
+ });
74
+ return;
75
+ }
76
+ if (msg.method === 'tools/call') {
77
+ const params = msg.params;
78
+ const name = params?.name;
79
+ const args = params?.arguments ?? {};
80
+ const handler = name ? handlers.get(name) : undefined;
81
+ if (!handler) {
82
+ send({ jsonrpc: '2.0', id, error: { code: -32601, message: `Unknown tool: ${name}` } });
83
+ return;
84
+ }
85
+ const result = await handler(args);
86
+ send({ jsonrpc: '2.0', id, result: { content: [{ type: 'text', text: JSON.stringify(result) }] } });
87
+ return;
88
+ }
89
+ if (msg.method === 'prompts/list') {
90
+ send({
91
+ jsonrpc: '2.0',
92
+ id,
93
+ result: {
94
+ prompts: [
95
+ { name: 'share_new_rule', title: 'Share a new rule', description: 'Guide to collect and publish a reusable rule' },
96
+ { name: 'share_problem_solution', title: 'Share a problem solution', description: 'Guide to collect and publish a problem solution' },
97
+ ],
98
+ },
99
+ });
100
+ return;
101
+ }
102
+ if (msg.method === 'prompts/get') {
103
+ const params = msg.params;
104
+ const name = params?.name ?? '';
105
+ if (name === 'share_new_rule') {
106
+ send({
107
+ jsonrpc: '2.0',
108
+ id,
109
+ result: {
110
+ messages: [
111
+ { role: 'user', content: { type: 'text', text: 'You are helping formalize a reusable rule. Collect: title, description, rule body, and optionally technologies/tags. Ask one question at a time. Then call post-rules with kind: "rule".' } },
112
+ ],
113
+ },
114
+ });
115
+ return;
116
+ }
117
+ if (name === 'share_problem_solution') {
118
+ send({
119
+ jsonrpc: '2.0',
120
+ id,
121
+ result: {
122
+ messages: [
123
+ { role: 'user', content: { type: 'text', text: 'You are helping share a problem solution. Collect: problem title, description, and solution text. Ask one question at a time. Then call post-rules with kind: "solution".' } },
124
+ ],
125
+ },
126
+ });
127
+ return;
128
+ }
129
+ send({ jsonrpc: '2.0', id, error: { code: -32602, message: 'Unknown prompt' } });
130
+ return;
131
+ }
132
+ send({ jsonrpc: '2.0', id, error: { code: -32601, message: 'Method not found' } });
133
+ }
134
+ catch (err) {
135
+ if (id !== undefined) {
136
+ send({
137
+ jsonrpc: '2.0',
138
+ id,
139
+ error: { code: -32603, message: err instanceof Error ? err.message : String(err) },
140
+ });
141
+ }
142
+ }
143
+ };
144
+ let buffer = '';
145
+ const queue = [];
146
+ let processing = false;
147
+ const processQueue = async () => {
148
+ if (processing || queue.length === 0)
149
+ return;
150
+ processing = true;
151
+ while (queue.length > 0) {
152
+ const msg = queue.shift();
153
+ await handleRequest(msg);
154
+ }
155
+ processing = false;
156
+ };
157
+ const onData = (chunk) => {
158
+ buffer += chunk.toString();
159
+ const lines = buffer.split('\n');
160
+ buffer = lines.pop() ?? '';
161
+ for (const line of lines) {
162
+ if (!line.trim())
163
+ continue;
164
+ try {
165
+ const msg = JSON.parse(line);
166
+ queue.push(msg);
167
+ void processQueue();
168
+ }
169
+ catch {
170
+ // ignore malformed
171
+ }
172
+ }
173
+ };
174
+ process.stdin.on('data', onData);
175
+ // Register tool implementations
176
+ handlers.set('search-rules', async (args) => {
177
+ const creds = loadCredentials();
178
+ if (!creds?.access_token)
179
+ return { error: 'Run bitcompass login first.' };
180
+ const query = args.query ?? '';
181
+ const kind = args.kind;
182
+ const limit = args.limit ?? 20;
183
+ const list = await searchRules(query, { kind, limit });
184
+ return { rules: list.map((r) => ({ id: r.id, title: r.title, kind: r.kind, snippet: r.body.slice(0, 200) })) };
185
+ });
186
+ handlers.set('search-solutions', async (args) => {
187
+ const creds = loadCredentials();
188
+ if (!creds?.access_token)
189
+ return { error: 'Run bitcompass login first.' };
190
+ const query = args.query ?? '';
191
+ const limit = args.limit ?? 20;
192
+ const list = await searchRules(query, { kind: 'solution', limit });
193
+ return { solutions: list.map((r) => ({ id: r.id, title: r.title, snippet: r.body.slice(0, 200) })) };
194
+ });
195
+ handlers.set('post-rules', async (args) => {
196
+ const creds = loadCredentials();
197
+ if (!creds?.access_token)
198
+ return { error: 'Run bitcompass login first.' };
199
+ const payload = {
200
+ kind: args.kind ?? 'rule',
201
+ title: args.title ?? 'Untitled',
202
+ description: args.description ?? '',
203
+ body: args.body ?? '',
204
+ context: args.context || undefined,
205
+ examples: Array.isArray(args.examples) ? args.examples : undefined,
206
+ technologies: Array.isArray(args.technologies) ? args.technologies : undefined,
207
+ };
208
+ const created = await insertRule(payload);
209
+ return { id: created.id, title: created.title, success: true };
210
+ });
211
+ return {
212
+ async connect() {
213
+ // Stdio listener already attached; keep process alive
214
+ },
215
+ };
216
+ }
217
+ export const startMcpServer = async () => {
218
+ const creds = loadCredentials();
219
+ if (!creds?.access_token) {
220
+ process.stderr.write('Not logged in. Run bitcompass login first.\n');
221
+ process.exit(1);
222
+ }
223
+ const server = createStdioServer();
224
+ await server.connect();
225
+ };
@@ -0,0 +1,36 @@
1
+ export type RuleKind = 'rule' | 'solution';
2
+ export interface Rule {
3
+ id: string;
4
+ kind: RuleKind;
5
+ title: string;
6
+ description: string;
7
+ body: string;
8
+ context?: string | null;
9
+ examples?: string[];
10
+ technologies?: string[];
11
+ user_id: string;
12
+ created_at: string;
13
+ updated_at: string;
14
+ }
15
+ export interface RuleInsert {
16
+ kind: RuleKind;
17
+ title: string;
18
+ description: string;
19
+ body: string;
20
+ context?: string | null;
21
+ examples?: string[];
22
+ technologies?: string[];
23
+ }
24
+ export interface StoredCredentials {
25
+ access_token: string;
26
+ refresh_token: string;
27
+ expires_at?: number;
28
+ user?: {
29
+ email?: string;
30
+ };
31
+ }
32
+ export interface BitcompassConfig {
33
+ apiUrl?: string;
34
+ supabaseUrl?: string;
35
+ supabaseAnonKey?: string;
36
+ }
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "bitcompass",
3
+ "version": "0.1.0",
4
+ "description": "BitCompass CLI - rules, solutions, and MCP server",
5
+ "type": "module",
6
+ "bin": { "bitcompass": "./dist/index.js" },
7
+ "scripts": {
8
+ "build": "tsc",
9
+ "dev": "tsc --watch",
10
+ "start": "node dist/index.js",
11
+ "prepare": "npm run build"
12
+ },
13
+ "engines": { "node": ">=18" },
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "https://github.com/company-compass/company-compass.git",
17
+ "directory": "packages/bitcompass-cli"
18
+ },
19
+ "license": "MIT",
20
+ "keywords": ["mcp", "cursor", "rules", "bitcompass", "cli", "supabase"],
21
+ "dependencies": {
22
+ "dotenv": "^16.4.5",
23
+ "@modelcontextprotocol/sdk": "^1.0.0",
24
+ "@supabase/supabase-js": "^2.45.0",
25
+ "chalk": "^5.3.0",
26
+ "commander": "^12.1.0",
27
+ "inquirer": "^9.2.22",
28
+ "ky": "^1.7.2",
29
+ "open": "^10.1.0",
30
+ "ora": "^8.1.1",
31
+ "zod": "^3.23.0"
32
+ },
33
+ "devDependencies": {
34
+ "@types/inquirer": "^9.0.7",
35
+ "@types/node": "^22.0.0",
36
+ "typescript": "^5.8.0"
37
+ },
38
+ "files": ["dist"]
39
+ }