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 +90 -0
- package/dist/api/client.d.ts +12 -0
- package/dist/api/client.js +89 -0
- package/dist/auth/config.d.ts +9 -0
- package/dist/auth/config.js +81 -0
- package/dist/commands/config-cmd.d.ts +3 -0
- package/dist/commands/config-cmd.js +33 -0
- package/dist/commands/login.d.ts +1 -0
- package/dist/commands/login.js +146 -0
- package/dist/commands/logout.d.ts +1 -0
- package/dist/commands/logout.js +6 -0
- package/dist/commands/mcp.d.ts +2 -0
- package/dist/commands/mcp.js +18 -0
- package/dist/commands/rules.d.ts +4 -0
- package/dist/commands/rules.js +105 -0
- package/dist/commands/solutions.d.ts +3 -0
- package/dist/commands/solutions.js +94 -0
- package/dist/commands/whoami.d.ts +1 -0
- package/dist/commands/whoami.js +16 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +53 -0
- package/dist/mcp/server.d.ts +1 -0
- package/dist/mcp/server.js +225 -0
- package/dist/types.d.ts +36 -0
- package/dist/types.js +1 -0
- package/package.json +39 -0
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,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,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,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,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
|
+
};
|
package/dist/index.d.ts
ADDED
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
|
+
};
|
package/dist/types.d.ts
ADDED
|
@@ -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
|
+
}
|