bitcompass 0.1.1 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -6
- package/dist/api/client.d.ts +7 -0
- package/dist/api/client.js +55 -25
- package/dist/commands/login.js +158 -6
- package/dist/mcp/server.js +29 -20
- package/dist/types.d.ts +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -54,16 +54,13 @@ Add the `bitcompass` entry under `mcpServers`:
|
|
|
54
54
|
"bitcompass": {
|
|
55
55
|
"type": "stdio",
|
|
56
56
|
"command": "bitcompass",
|
|
57
|
-
"args": ["mcp", "start"]
|
|
58
|
-
"env": {
|
|
59
|
-
"BITCOMPASS_CONFIG_DIR": "~/.bitcompass"
|
|
60
|
-
}
|
|
57
|
+
"args": ["mcp", "start"]
|
|
61
58
|
}
|
|
62
59
|
}
|
|
63
60
|
}
|
|
64
61
|
```
|
|
65
62
|
|
|
66
|
-
|
|
63
|
+
Run `bitcompass login` before using MCP. If you added the MCP before logging in, restart the MCP server in Cursor after logging in.
|
|
67
64
|
|
|
68
65
|
### Development (this repo)
|
|
69
66
|
|
|
@@ -73,7 +70,7 @@ This repo includes **`.cursor/mcp.json`** so Cursor points at the local CLI when
|
|
|
73
70
|
cd packages/bitcompass-cli && npm run build && bitcompass login
|
|
74
71
|
```
|
|
75
72
|
|
|
76
|
-
**Manual (local path):** Settings → MCP → stdio, Command **node**, Args **path/to/packages/bitcompass-cli/dist/index.js** **mcp** **start**.
|
|
73
|
+
**Manual (local path):** Settings → MCP → stdio, Command **node**, Args **path/to/packages/bitcompass-cli/dist/index.js** **mcp** **start**.
|
|
77
74
|
|
|
78
75
|
Tools: `search-rules`, `search-solutions`, `post-rules`. Prompts: `share_new_rule`, `share_problem_solution`.
|
|
79
76
|
|
package/dist/api/client.d.ts
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import { type SupabaseClient } from '@supabase/supabase-js';
|
|
2
2
|
import type { Rule, RuleInsert } from '../types.js';
|
|
3
|
+
/** Shown when MCP is used before logging in; instructs user to login and restart MCP. */
|
|
4
|
+
export declare const AUTH_REQUIRED_MSG = "BitCompass needs authentication. Run `bitcompass login`, then restart the MCP server in your editor.";
|
|
5
|
+
/** Shown when Supabase URL/key are not set; instructs config then login then restart MCP. */
|
|
6
|
+
export declare const NOT_CONFIGURED_MSG = "BitCompass is not configured. Run `bitcompass config set supabaseUrl` and `bitcompass config set supabaseAnonKey`, then `bitcompass login`, then restart the MCP server in your editor.";
|
|
7
|
+
/** Client for writes and authenticated reads (uses token when available). */
|
|
3
8
|
export declare const getSupabaseClient: () => SupabaseClient | null;
|
|
9
|
+
/** Client for public read-only (rules/solutions). Works without login when RLS allows public select. */
|
|
10
|
+
export declare const getSupabaseClientForRead: () => SupabaseClient | null;
|
|
4
11
|
export declare const fetchRules: (kind?: "rule" | "solution") => Promise<Rule[]>;
|
|
5
12
|
export declare const searchRules: (queryText: string, options?: {
|
|
6
13
|
kind?: "rule" | "solution";
|
package/dist/api/client.js
CHANGED
|
@@ -1,45 +1,75 @@
|
|
|
1
1
|
import { createClient } from '@supabase/supabase-js';
|
|
2
2
|
import { loadConfig, loadCredentials } from '../auth/config.js';
|
|
3
3
|
import { DEFAULT_SUPABASE_ANON_KEY, DEFAULT_SUPABASE_URL } from '../auth/defaults.js';
|
|
4
|
-
|
|
4
|
+
/** Shown when MCP is used before logging in; instructs user to login and restart MCP. */
|
|
5
|
+
export const AUTH_REQUIRED_MSG = 'BitCompass needs authentication. Run `bitcompass login`, then restart the MCP server in your editor.';
|
|
6
|
+
/** Shown when Supabase URL/key are not set; instructs config then login then restart MCP. */
|
|
7
|
+
export const NOT_CONFIGURED_MSG = 'BitCompass is not configured. Run `bitcompass config set supabaseUrl` and `bitcompass config set supabaseAnonKey`, then `bitcompass login`, then restart the MCP server in your editor.';
|
|
8
|
+
const isAuthError = (err) => {
|
|
9
|
+
const m = (err.message ?? '').toLowerCase();
|
|
10
|
+
const c = err.code ?? '';
|
|
11
|
+
return (c === 'PGRST301' ||
|
|
12
|
+
c === '401' ||
|
|
13
|
+
m.includes('jwt') ||
|
|
14
|
+
m.includes('row-level security') ||
|
|
15
|
+
m.includes('permission denied') ||
|
|
16
|
+
m.includes('not authenticated'));
|
|
17
|
+
};
|
|
18
|
+
const getSupabaseUrlAndKey = () => {
|
|
5
19
|
const config = loadConfig();
|
|
6
|
-
const creds = loadCredentials();
|
|
7
20
|
const url = config.supabaseUrl ??
|
|
8
21
|
process.env.BITCOMPASS_SUPABASE_URL ??
|
|
9
22
|
DEFAULT_SUPABASE_URL;
|
|
10
23
|
const key = config.supabaseAnonKey ??
|
|
11
24
|
process.env.BITCOMPASS_SUPABASE_ANON_KEY ??
|
|
12
25
|
DEFAULT_SUPABASE_ANON_KEY;
|
|
13
|
-
if (!url || !key)
|
|
26
|
+
if (!url || !key)
|
|
14
27
|
return null;
|
|
15
|
-
}
|
|
28
|
+
return { url, key };
|
|
29
|
+
};
|
|
30
|
+
/** Client for writes and authenticated reads (uses token when available). */
|
|
31
|
+
export const getSupabaseClient = () => {
|
|
32
|
+
const pair = getSupabaseUrlAndKey();
|
|
33
|
+
if (!pair)
|
|
34
|
+
return null;
|
|
35
|
+
const creds = loadCredentials();
|
|
16
36
|
const accessToken = creds?.access_token;
|
|
17
|
-
|
|
37
|
+
return createClient(pair.url, pair.key, {
|
|
38
|
+
global: accessToken
|
|
39
|
+
? { headers: { Authorization: `Bearer ${accessToken}` } }
|
|
40
|
+
: undefined,
|
|
41
|
+
});
|
|
42
|
+
};
|
|
43
|
+
/** Client for public read-only (rules/solutions). Works without login when RLS allows public select. */
|
|
44
|
+
export const getSupabaseClientForRead = () => {
|
|
45
|
+
const pair = getSupabaseUrlAndKey();
|
|
46
|
+
if (!pair)
|
|
47
|
+
return null;
|
|
48
|
+
const creds = loadCredentials();
|
|
49
|
+
const accessToken = creds?.access_token;
|
|
50
|
+
return createClient(pair.url, pair.key, {
|
|
18
51
|
global: accessToken
|
|
19
52
|
? { headers: { Authorization: `Bearer ${accessToken}` } }
|
|
20
53
|
: undefined,
|
|
21
54
|
});
|
|
22
|
-
return client;
|
|
23
55
|
};
|
|
24
56
|
export const fetchRules = async (kind) => {
|
|
25
|
-
const client =
|
|
26
|
-
if (!client)
|
|
27
|
-
throw new Error(
|
|
28
|
-
}
|
|
57
|
+
const client = getSupabaseClientForRead();
|
|
58
|
+
if (!client)
|
|
59
|
+
throw new Error(NOT_CONFIGURED_MSG);
|
|
29
60
|
let query = client.from('rules').select('*').order('created_at', { ascending: false });
|
|
30
61
|
if (kind) {
|
|
31
62
|
query = query.eq('kind', kind);
|
|
32
63
|
}
|
|
33
64
|
const { data, error } = await query;
|
|
34
65
|
if (error)
|
|
35
|
-
throw new Error(error.message);
|
|
66
|
+
throw new Error(isAuthError(error) ? AUTH_REQUIRED_MSG : error.message);
|
|
36
67
|
return (data ?? []);
|
|
37
68
|
};
|
|
38
69
|
export const searchRules = async (queryText, options = {}) => {
|
|
39
|
-
const client =
|
|
40
|
-
if (!client)
|
|
41
|
-
throw new Error(
|
|
42
|
-
}
|
|
70
|
+
const client = getSupabaseClientForRead();
|
|
71
|
+
if (!client)
|
|
72
|
+
throw new Error(NOT_CONFIGURED_MSG);
|
|
43
73
|
let query = client
|
|
44
74
|
.from('rules')
|
|
45
75
|
.select('*')
|
|
@@ -51,44 +81,44 @@ export const searchRules = async (queryText, options = {}) => {
|
|
|
51
81
|
}
|
|
52
82
|
const { data, error } = await query;
|
|
53
83
|
if (error)
|
|
54
|
-
throw new Error(error.message);
|
|
84
|
+
throw new Error(isAuthError(error) ? AUTH_REQUIRED_MSG : error.message);
|
|
55
85
|
return (data ?? []);
|
|
56
86
|
};
|
|
57
87
|
export const getRuleById = async (id) => {
|
|
58
|
-
const client =
|
|
88
|
+
const client = getSupabaseClientForRead();
|
|
59
89
|
if (!client)
|
|
60
|
-
throw new Error(
|
|
90
|
+
throw new Error(NOT_CONFIGURED_MSG);
|
|
61
91
|
const { data, error } = await client.from('rules').select('*').eq('id', id).single();
|
|
62
92
|
if (error) {
|
|
63
93
|
if (error.code === 'PGRST116')
|
|
64
94
|
return null;
|
|
65
|
-
throw new Error(error.message);
|
|
95
|
+
throw new Error(isAuthError(error) ? AUTH_REQUIRED_MSG : error.message);
|
|
66
96
|
}
|
|
67
97
|
return data;
|
|
68
98
|
};
|
|
69
99
|
export const insertRule = async (rule) => {
|
|
70
100
|
const client = getSupabaseClient();
|
|
71
101
|
if (!client)
|
|
72
|
-
throw new Error(
|
|
102
|
+
throw new Error(NOT_CONFIGURED_MSG);
|
|
73
103
|
const { data, error } = await client.from('rules').insert(rule).select().single();
|
|
74
104
|
if (error)
|
|
75
|
-
throw new Error(error.message);
|
|
105
|
+
throw new Error(isAuthError(error) ? AUTH_REQUIRED_MSG : error.message);
|
|
76
106
|
return data;
|
|
77
107
|
};
|
|
78
108
|
export const updateRule = async (id, updates) => {
|
|
79
109
|
const client = getSupabaseClient();
|
|
80
110
|
if (!client)
|
|
81
|
-
throw new Error(
|
|
111
|
+
throw new Error(NOT_CONFIGURED_MSG);
|
|
82
112
|
const { data, error } = await client.from('rules').update(updates).eq('id', id).select().single();
|
|
83
113
|
if (error)
|
|
84
|
-
throw new Error(error.message);
|
|
114
|
+
throw new Error(isAuthError(error) ? AUTH_REQUIRED_MSG : error.message);
|
|
85
115
|
return data;
|
|
86
116
|
};
|
|
87
117
|
export const deleteRule = async (id) => {
|
|
88
118
|
const client = getSupabaseClient();
|
|
89
119
|
if (!client)
|
|
90
|
-
throw new Error(
|
|
120
|
+
throw new Error(NOT_CONFIGURED_MSG);
|
|
91
121
|
const { error } = await client.from('rules').delete().eq('id', id);
|
|
92
122
|
if (error)
|
|
93
|
-
throw new Error(error.message);
|
|
123
|
+
throw new Error(isAuthError(error) ? AUTH_REQUIRED_MSG : error.message);
|
|
94
124
|
};
|
package/dist/commands/login.js
CHANGED
|
@@ -6,6 +6,158 @@ import ora from 'ora';
|
|
|
6
6
|
import { getTokenFilePath, loadConfig, saveCredentials } from '../auth/config.js';
|
|
7
7
|
import { DEFAULT_SUPABASE_ANON_KEY, DEFAULT_SUPABASE_URL } from '../auth/defaults.js';
|
|
8
8
|
const CALLBACK_PORT = 38473;
|
|
9
|
+
/** Design tokens matching src/index.css (light theme) */
|
|
10
|
+
const STYLES = {
|
|
11
|
+
background: 'hsl(0, 0%, 98%)',
|
|
12
|
+
foreground: 'hsl(222, 47%, 11%)',
|
|
13
|
+
primary: 'hsl(221, 83%, 53%)',
|
|
14
|
+
primaryForeground: 'hsl(0, 0%, 100%)',
|
|
15
|
+
muted: 'hsl(220, 9%, 46%)',
|
|
16
|
+
card: 'hsl(0, 0%, 100%)',
|
|
17
|
+
border: 'hsl(220, 13%, 91%)',
|
|
18
|
+
radius: '0.625rem',
|
|
19
|
+
shadow: '0 10px 15px -3px hsl(220 13% 11% / 0.08), 0 4px 6px -4px hsl(220 13% 11% / 0.05)',
|
|
20
|
+
};
|
|
21
|
+
const CALLBACK_SUCCESS_HTML = `<!DOCTYPE html>
|
|
22
|
+
<html lang="en">
|
|
23
|
+
<head>
|
|
24
|
+
<meta charset="UTF-8" />
|
|
25
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
26
|
+
<title>Logged in — Bitcompass</title>
|
|
27
|
+
<style>
|
|
28
|
+
* { box-sizing: border-box; }
|
|
29
|
+
body {
|
|
30
|
+
margin: 0;
|
|
31
|
+
min-height: 100vh;
|
|
32
|
+
display: flex;
|
|
33
|
+
align-items: center;
|
|
34
|
+
justify-content: center;
|
|
35
|
+
background: ${STYLES.background};
|
|
36
|
+
color: ${STYLES.foreground};
|
|
37
|
+
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
38
|
+
-webkit-font-smoothing: antialiased;
|
|
39
|
+
padding: 1rem;
|
|
40
|
+
}
|
|
41
|
+
.card {
|
|
42
|
+
width: 100%;
|
|
43
|
+
max-width: 28rem;
|
|
44
|
+
background: ${STYLES.card};
|
|
45
|
+
border: 1px solid ${STYLES.border};
|
|
46
|
+
border-radius: ${STYLES.radius};
|
|
47
|
+
box-shadow: ${STYLES.shadow};
|
|
48
|
+
padding: 2rem;
|
|
49
|
+
text-align: center;
|
|
50
|
+
animation: fadeIn 0.35s ease-out;
|
|
51
|
+
}
|
|
52
|
+
@keyframes fadeIn {
|
|
53
|
+
from { opacity: 0; transform: translateY(8px); }
|
|
54
|
+
to { opacity: 1; transform: translateY(0); }
|
|
55
|
+
}
|
|
56
|
+
.icon-wrap {
|
|
57
|
+
display: inline-flex;
|
|
58
|
+
align-items: center;
|
|
59
|
+
justify-content: center;
|
|
60
|
+
width: 4rem;
|
|
61
|
+
height: 4rem;
|
|
62
|
+
border-radius: 1rem;
|
|
63
|
+
background: ${STYLES.primary};
|
|
64
|
+
color: ${STYLES.primaryForeground};
|
|
65
|
+
margin-bottom: 1.25rem;
|
|
66
|
+
}
|
|
67
|
+
.icon-wrap svg { width: 2rem; height: 2rem; }
|
|
68
|
+
h1 {
|
|
69
|
+
margin: 0 0 0.5rem;
|
|
70
|
+
font-size: 1.5rem;
|
|
71
|
+
font-weight: 700;
|
|
72
|
+
}
|
|
73
|
+
.muted {
|
|
74
|
+
color: ${STYLES.muted};
|
|
75
|
+
font-size: 0.875rem;
|
|
76
|
+
line-height: 1.5;
|
|
77
|
+
margin: 0;
|
|
78
|
+
}
|
|
79
|
+
.hint {
|
|
80
|
+
margin-top: 1.25rem;
|
|
81
|
+
padding-top: 1.25rem;
|
|
82
|
+
border-top: 1px solid ${STYLES.border};
|
|
83
|
+
font-size: 0.8125rem;
|
|
84
|
+
color: ${STYLES.muted};
|
|
85
|
+
}
|
|
86
|
+
</style>
|
|
87
|
+
</head>
|
|
88
|
+
<body>
|
|
89
|
+
<div class="card">
|
|
90
|
+
<div class="icon-wrap" aria-hidden="true">
|
|
91
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
92
|
+
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/>
|
|
93
|
+
<polyline points="22 4 12 14.01 9 11.01"/>
|
|
94
|
+
</svg>
|
|
95
|
+
</div>
|
|
96
|
+
<h1>You're all set</h1>
|
|
97
|
+
<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
|
+
<p class="hint">Return to your terminal to continue.</p>
|
|
99
|
+
</div>
|
|
100
|
+
</body>
|
|
101
|
+
</html>`;
|
|
102
|
+
const escapeHtml = (s) => s
|
|
103
|
+
.replace(/&/g, '&')
|
|
104
|
+
.replace(/</g, '<')
|
|
105
|
+
.replace(/>/g, '>')
|
|
106
|
+
.replace(/"/g, '"')
|
|
107
|
+
.replace(/'/g, ''');
|
|
108
|
+
const callbackPage = (opts) => {
|
|
109
|
+
const iconColor = opts.isError ? 'hsl(0, 84%, 60%)' : STYLES.primary;
|
|
110
|
+
const safeMessage = escapeHtml(opts.message);
|
|
111
|
+
return `<!DOCTYPE html>
|
|
112
|
+
<html lang="en">
|
|
113
|
+
<head>
|
|
114
|
+
<meta charset="UTF-8" />
|
|
115
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
116
|
+
<title>${opts.title} — Bitcompass</title>
|
|
117
|
+
<style>
|
|
118
|
+
* { box-sizing: border-box; }
|
|
119
|
+
body {
|
|
120
|
+
margin: 0;
|
|
121
|
+
min-height: 100vh;
|
|
122
|
+
display: flex;
|
|
123
|
+
align-items: center;
|
|
124
|
+
justify-content: center;
|
|
125
|
+
background: ${STYLES.background};
|
|
126
|
+
color: ${STYLES.foreground};
|
|
127
|
+
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
128
|
+
-webkit-font-smoothing: antialiased;
|
|
129
|
+
padding: 1rem;
|
|
130
|
+
}
|
|
131
|
+
.card {
|
|
132
|
+
width: 100%;
|
|
133
|
+
max-width: 28rem;
|
|
134
|
+
background: ${STYLES.card};
|
|
135
|
+
border: 1px solid ${STYLES.border};
|
|
136
|
+
border-radius: ${STYLES.radius};
|
|
137
|
+
box-shadow: ${STYLES.shadow};
|
|
138
|
+
padding: 2rem;
|
|
139
|
+
text-align: center;
|
|
140
|
+
}
|
|
141
|
+
.icon-wrap { display: inline-flex; align-items: center; justify-content: center; width: 4rem; height: 4rem; border-radius: 1rem; margin-bottom: 1.25rem; }
|
|
142
|
+
.icon-wrap svg { width: 2rem; height: 2rem; }
|
|
143
|
+
.icon-wrap { background: ${iconColor}; color: ${STYLES.primaryForeground}; }
|
|
144
|
+
h1 { margin: 0 0 0.5rem; font-size: 1.25rem; font-weight: 700; }
|
|
145
|
+
.muted { color: ${STYLES.muted}; font-size: 0.875rem; line-height: 1.5; margin: 0; }
|
|
146
|
+
</style>
|
|
147
|
+
</head>
|
|
148
|
+
<body>
|
|
149
|
+
<div class="card">
|
|
150
|
+
<div class="icon-wrap" aria-hidden="true">
|
|
151
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
152
|
+
<circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/>
|
|
153
|
+
</svg>
|
|
154
|
+
</div>
|
|
155
|
+
<h1>${opts.title}</h1>
|
|
156
|
+
<p class="muted">${safeMessage}</p>
|
|
157
|
+
</div>
|
|
158
|
+
</body>
|
|
159
|
+
</html>`;
|
|
160
|
+
};
|
|
9
161
|
const createInMemoryStorage = () => {
|
|
10
162
|
const store = new Map();
|
|
11
163
|
return {
|
|
@@ -48,7 +200,7 @@ export const runLogin = async () => {
|
|
|
48
200
|
const errorDesc = u.searchParams.get('error_description');
|
|
49
201
|
if (errorDesc) {
|
|
50
202
|
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
51
|
-
res.end(
|
|
203
|
+
res.end(callbackPage({ title: 'Login failed', message: errorDesc, isError: true }));
|
|
52
204
|
spinner.fail(chalk.red('Login failed: ' + errorDesc));
|
|
53
205
|
server.close();
|
|
54
206
|
reject(new Error(errorDesc));
|
|
@@ -56,7 +208,7 @@ export const runLogin = async () => {
|
|
|
56
208
|
}
|
|
57
209
|
if (!code) {
|
|
58
210
|
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
59
|
-
res.end('
|
|
211
|
+
res.end(callbackPage({ title: 'No authorization code', message: 'No authorization code was received. Please try logging in again.', isError: true }));
|
|
60
212
|
spinner.fail(chalk.red('No code received. Try again.'));
|
|
61
213
|
server.close();
|
|
62
214
|
reject(new Error('No code'));
|
|
@@ -66,7 +218,7 @@ export const runLogin = async () => {
|
|
|
66
218
|
const { data, error } = await supabase.auth.exchangeCodeForSession(code);
|
|
67
219
|
if (error) {
|
|
68
220
|
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
69
|
-
res.end(
|
|
221
|
+
res.end(callbackPage({ title: 'Login failed', message: error.message, isError: true }));
|
|
70
222
|
spinner.fail(chalk.red('Login failed: ' + error.message));
|
|
71
223
|
server.close();
|
|
72
224
|
reject(error);
|
|
@@ -75,7 +227,7 @@ export const runLogin = async () => {
|
|
|
75
227
|
const session = data?.session;
|
|
76
228
|
if (!session?.access_token) {
|
|
77
229
|
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
78
|
-
res.end('
|
|
230
|
+
res.end(callbackPage({ title: 'No session', message: 'No session was received. Please try again.', isError: true }));
|
|
79
231
|
spinner.fail(chalk.red('No session.'));
|
|
80
232
|
server.close();
|
|
81
233
|
reject(new Error('No session'));
|
|
@@ -89,7 +241,7 @@ export const runLogin = async () => {
|
|
|
89
241
|
const tokenPath = getTokenFilePath();
|
|
90
242
|
saveCredentials(creds);
|
|
91
243
|
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
92
|
-
res.end(
|
|
244
|
+
res.end(CALLBACK_SUCCESS_HTML);
|
|
93
245
|
spinner.succeed(chalk.green('Logged in successfully.'));
|
|
94
246
|
console.log(chalk.dim('Credentials saved to:'), tokenPath);
|
|
95
247
|
server.close();
|
|
@@ -97,7 +249,7 @@ export const runLogin = async () => {
|
|
|
97
249
|
}
|
|
98
250
|
catch (e) {
|
|
99
251
|
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
100
|
-
res.end(
|
|
252
|
+
res.end(callbackPage({ title: 'Error', message: e instanceof Error ? e.message : String(e), isError: true }));
|
|
101
253
|
spinner.fail('Login failed.');
|
|
102
254
|
console.error(chalk.red(e instanceof Error ? e.message : String(e)));
|
|
103
255
|
server.close();
|
package/dist/mcp/server.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { insertRule, searchRules } from '../api/client.js';
|
|
1
|
+
import { AUTH_REQUIRED_MSG, insertRule, searchRules } from '../api/client.js';
|
|
2
2
|
import { loadCredentials } from '../auth/config.js';
|
|
3
3
|
function createStdioServer() {
|
|
4
4
|
const handlers = new Map();
|
|
@@ -172,30 +172,36 @@ function createStdioServer() {
|
|
|
172
172
|
}
|
|
173
173
|
};
|
|
174
174
|
process.stdin.on('data', onData);
|
|
175
|
-
// Register tool implementations
|
|
175
|
+
// Register tool implementations (search is public; post requires login)
|
|
176
176
|
handlers.set('search-rules', async (args) => {
|
|
177
|
-
const creds = loadCredentials();
|
|
178
|
-
if (!creds?.access_token)
|
|
179
|
-
return { error: 'Run bitcompass login first.' };
|
|
180
177
|
const query = args.query ?? '';
|
|
181
178
|
const kind = args.kind;
|
|
182
179
|
const limit = args.limit ?? 20;
|
|
183
|
-
|
|
184
|
-
|
|
180
|
+
try {
|
|
181
|
+
const list = await searchRules(query, { kind, limit });
|
|
182
|
+
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) })) };
|
|
183
|
+
}
|
|
184
|
+
catch (e) {
|
|
185
|
+
const msg = e instanceof Error ? e.message : 'Search failed.';
|
|
186
|
+
return { error: msg };
|
|
187
|
+
}
|
|
185
188
|
});
|
|
186
189
|
handlers.set('search-solutions', async (args) => {
|
|
187
|
-
const creds = loadCredentials();
|
|
188
|
-
if (!creds?.access_token)
|
|
189
|
-
return { error: 'Run bitcompass login first.' };
|
|
190
190
|
const query = args.query ?? '';
|
|
191
191
|
const limit = args.limit ?? 20;
|
|
192
|
-
|
|
193
|
-
|
|
192
|
+
try {
|
|
193
|
+
const list = await searchRules(query, { kind: 'solution', limit });
|
|
194
|
+
return { solutions: list.map((r) => ({ id: r.id, title: r.title, author: r.author_display_name ?? null, snippet: r.body.slice(0, 200) })) };
|
|
195
|
+
}
|
|
196
|
+
catch (e) {
|
|
197
|
+
const msg = e instanceof Error ? e.message : 'Search failed.';
|
|
198
|
+
return { error: msg };
|
|
199
|
+
}
|
|
194
200
|
});
|
|
195
201
|
handlers.set('post-rules', async (args) => {
|
|
196
202
|
const creds = loadCredentials();
|
|
197
203
|
if (!creds?.access_token)
|
|
198
|
-
return { error:
|
|
204
|
+
return { error: AUTH_REQUIRED_MSG };
|
|
199
205
|
const payload = {
|
|
200
206
|
kind: args.kind ?? 'rule',
|
|
201
207
|
title: args.title ?? 'Untitled',
|
|
@@ -205,8 +211,14 @@ function createStdioServer() {
|
|
|
205
211
|
examples: Array.isArray(args.examples) ? args.examples : undefined,
|
|
206
212
|
technologies: Array.isArray(args.technologies) ? args.technologies : undefined,
|
|
207
213
|
};
|
|
208
|
-
|
|
209
|
-
|
|
214
|
+
try {
|
|
215
|
+
const created = await insertRule(payload);
|
|
216
|
+
return { id: created.id, title: created.title, success: true };
|
|
217
|
+
}
|
|
218
|
+
catch (e) {
|
|
219
|
+
const msg = e instanceof Error ? e.message : 'Failed to publish rule.';
|
|
220
|
+
return { error: msg };
|
|
221
|
+
}
|
|
210
222
|
});
|
|
211
223
|
return {
|
|
212
224
|
async connect() {
|
|
@@ -215,11 +227,8 @@ function createStdioServer() {
|
|
|
215
227
|
};
|
|
216
228
|
}
|
|
217
229
|
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
230
|
const server = createStdioServer();
|
|
224
231
|
await server.connect();
|
|
232
|
+
// Do not exit when not logged in: Cursor needs the process alive to complete
|
|
233
|
+
// the MCP handshake. Tools return an auth message (run bitcompass login, then restart MCP) when needed.
|
|
225
234
|
};
|
package/dist/types.d.ts
CHANGED