bitcompass 0.3.8 → 0.4.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/dist/api/client.js +1 -12
- package/dist/commands/commands.d.ts +3 -1
- package/dist/commands/commands.js +4 -32
- package/dist/commands/log.d.ts +7 -5
- package/dist/commands/log.js +63 -66
- package/dist/commands/login.js +48 -3
- package/dist/commands/rules.d.ts +3 -1
- package/dist/commands/rules.js +4 -46
- package/dist/commands/share.d.ts +19 -0
- package/dist/commands/share.js +137 -0
- package/dist/commands/skills.d.ts +3 -1
- package/dist/commands/skills.js +4 -32
- package/dist/commands/solutions.d.ts +3 -1
- package/dist/commands/solutions.js +4 -32
- package/dist/index.js +40 -4
- package/dist/lib/mdc-format.d.ts +13 -2
- package/dist/lib/mdc-format.js +46 -2
- package/dist/lib/rule-cache.js +2 -4
- package/dist/lib/rule-file-ops.js +3 -5
- package/dist/lib/share-types.d.ts +12 -0
- package/dist/lib/share-types.js +41 -0
- package/dist/mcp/server.js +31 -4
- package/dist/types.d.ts +3 -0
- package/package.json +1 -1
- package/scripts/postinstall.js +4 -6
package/dist/api/client.js
CHANGED
|
@@ -41,18 +41,7 @@ export const getSupabaseClient = () => {
|
|
|
41
41
|
});
|
|
42
42
|
};
|
|
43
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, {
|
|
51
|
-
global: accessToken
|
|
52
|
-
? { headers: { Authorization: `Bearer ${accessToken}` } }
|
|
53
|
-
: undefined,
|
|
54
|
-
});
|
|
55
|
-
};
|
|
44
|
+
export const getSupabaseClientForRead = getSupabaseClient;
|
|
56
45
|
export const fetchRules = async (kind) => {
|
|
57
46
|
const client = getSupabaseClientForRead();
|
|
58
47
|
if (!client)
|
|
@@ -8,4 +8,6 @@ export declare const runCommandsPull: (id?: string, options?: {
|
|
|
8
8
|
global?: boolean;
|
|
9
9
|
copy?: boolean;
|
|
10
10
|
}) => Promise<void>;
|
|
11
|
-
export declare const runCommandsPush: (file?: string
|
|
11
|
+
export declare const runCommandsPush: (file?: string, options?: {
|
|
12
|
+
projectId?: string;
|
|
13
|
+
}) => Promise<void>;
|
|
@@ -2,9 +2,10 @@ import inquirer from 'inquirer';
|
|
|
2
2
|
import ora from 'ora';
|
|
3
3
|
import chalk from 'chalk';
|
|
4
4
|
import { loadCredentials } from '../auth/config.js';
|
|
5
|
-
import { searchRules, fetchRules, getRuleById
|
|
5
|
+
import { searchRules, fetchRules, getRuleById } from '../api/client.js';
|
|
6
6
|
import { pullRuleToFile } from '../lib/rule-file-ops.js';
|
|
7
7
|
import { formatList, shouldUseTable } from '../lib/list-format.js';
|
|
8
|
+
import { runSharePush } from './share.js';
|
|
8
9
|
export const runCommandsSearch = async (query, options) => {
|
|
9
10
|
if (!loadCredentials()) {
|
|
10
11
|
console.error(chalk.red('Not logged in. Run bitcompass login.'));
|
|
@@ -95,35 +96,6 @@ export const runCommandsPull = async (id, options) => {
|
|
|
95
96
|
process.exit(message.includes('not found') ? 2 : 1);
|
|
96
97
|
}
|
|
97
98
|
};
|
|
98
|
-
export const runCommandsPush = async (file) => {
|
|
99
|
-
|
|
100
|
-
console.error(chalk.red('Not logged in. Run bitcompass login.'));
|
|
101
|
-
process.exit(1);
|
|
102
|
-
}
|
|
103
|
-
let payload;
|
|
104
|
-
if (file) {
|
|
105
|
-
const { readFileSync } = await import('fs');
|
|
106
|
-
const raw = readFileSync(file, 'utf-8');
|
|
107
|
-
try {
|
|
108
|
-
payload = JSON.parse(raw);
|
|
109
|
-
payload.kind = 'command';
|
|
110
|
-
}
|
|
111
|
-
catch {
|
|
112
|
-
const lines = raw.split('\n');
|
|
113
|
-
const title = lines[0].replace(/^#\s*/, '') || 'Untitled';
|
|
114
|
-
payload = { kind: 'command', title, description: '', body: raw };
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
else {
|
|
118
|
-
const answers = await inquirer.prompt([
|
|
119
|
-
{ name: 'title', message: 'Command title', type: 'input', default: 'Untitled' },
|
|
120
|
-
{ name: 'description', message: 'Description', type: 'input', default: '' },
|
|
121
|
-
{ name: 'body', message: 'Command content', type: 'editor', default: '' },
|
|
122
|
-
]);
|
|
123
|
-
payload = { kind: 'command', title: answers.title, description: answers.description, body: answers.body };
|
|
124
|
-
}
|
|
125
|
-
const spinner = ora('Publishing command…').start();
|
|
126
|
-
const created = await insertRule(payload);
|
|
127
|
-
spinner.succeed(chalk.green('Published command ') + created.id);
|
|
128
|
-
console.log(chalk.dim(created.title));
|
|
99
|
+
export const runCommandsPush = async (file, options) => {
|
|
100
|
+
await runSharePush(file, { kind: 'command', projectId: options?.projectId });
|
|
129
101
|
};
|
package/dist/commands/log.d.ts
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import type { TimeFrame } from '../lib/git-analysis.js';
|
|
2
2
|
export type LogProgressStep = 'analyzing' | 'pushing';
|
|
3
|
+
type PeriodBounds = {
|
|
4
|
+
period_start: string;
|
|
5
|
+
period_end: string;
|
|
6
|
+
since: string;
|
|
7
|
+
};
|
|
3
8
|
/**
|
|
4
9
|
* Shared logic: resolve repo, compute period, gather summary + git analysis, insert log.
|
|
5
10
|
* Used by both CLI and MCP. Returns the created log id or throws.
|
|
@@ -11,11 +16,7 @@ export declare const buildAndPushActivityLog: (timeFrame: TimeFrame, cwd: string
|
|
|
11
16
|
/**
|
|
12
17
|
* Push an activity log for a custom date or date range. timeFrame is used for display (day/week/month).
|
|
13
18
|
*/
|
|
14
|
-
export declare const buildAndPushActivityLogWithPeriod: (period: {
|
|
15
|
-
period_start: string;
|
|
16
|
-
period_end: string;
|
|
17
|
-
since: string;
|
|
18
|
-
}, timeFrame: TimeFrame, cwd: string, onProgress?: (step: LogProgressStep) => void) => Promise<{
|
|
19
|
+
export declare const buildAndPushActivityLogWithPeriod: (period: PeriodBounds, timeFrame: TimeFrame, cwd: string, onProgress?: (step: LogProgressStep) => void) => Promise<{
|
|
19
20
|
id: string;
|
|
20
21
|
}>;
|
|
21
22
|
/** Parse argv for log: [start] or [start, end] or [start, '-', end]. Returns { start, end } or null for interactive. */
|
|
@@ -28,3 +29,4 @@ export declare class ValidationError extends Error {
|
|
|
28
29
|
constructor(message: string);
|
|
29
30
|
}
|
|
30
31
|
export declare const runLog: (args?: string[]) => Promise<void>;
|
|
32
|
+
export {};
|
package/dist/commands/log.js
CHANGED
|
@@ -1,23 +1,14 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
1
2
|
import inquirer from 'inquirer';
|
|
2
3
|
import ora from 'ora';
|
|
3
|
-
import chalk from 'chalk';
|
|
4
4
|
import { insertActivityLog } from '../api/client.js';
|
|
5
5
|
import { loadCredentials } from '../auth/config.js';
|
|
6
|
-
import {
|
|
7
|
-
/**
|
|
8
|
-
|
|
9
|
-
* Used by both CLI and MCP. Returns the created log id or throws.
|
|
10
|
-
* Optional onProgress callback for CLI to show step-wise spinner (e.g. analyzing → pushing).
|
|
11
|
-
*/
|
|
12
|
-
export const buildAndPushActivityLog = async (timeFrame, cwd, onProgress) => {
|
|
13
|
-
const repoRoot = getRepoRoot(cwd);
|
|
14
|
-
if (!repoRoot) {
|
|
15
|
-
throw new Error('Not a git repository. Run from a project with git or pass a valid repo path.');
|
|
16
|
-
}
|
|
17
|
-
const period = getPeriodForTimeFrame(timeFrame);
|
|
6
|
+
import { getGitAnalysis, getPeriodForCustomDates, getPeriodForTimeFrame, getRepoRoot, getRepoSummary, parseDate, } from '../lib/git-analysis.js';
|
|
7
|
+
/** Core logic: gather summary + git analysis, insert log. Shared by both build functions. */
|
|
8
|
+
const buildAndPushCore = async (repoRoot, period, timeFrame, onProgress) => {
|
|
18
9
|
onProgress?.('analyzing');
|
|
19
10
|
const repo_summary = getRepoSummary(repoRoot);
|
|
20
|
-
const git_analysis = getGitAnalysis(repoRoot, period.since);
|
|
11
|
+
const git_analysis = getGitAnalysis(repoRoot, period.since, period.period_end);
|
|
21
12
|
const payload = {
|
|
22
13
|
time_frame: timeFrame,
|
|
23
14
|
period_start: period.period_start,
|
|
@@ -29,6 +20,19 @@ export const buildAndPushActivityLog = async (timeFrame, cwd, onProgress) => {
|
|
|
29
20
|
const created = await insertActivityLog(payload);
|
|
30
21
|
return { id: created.id };
|
|
31
22
|
};
|
|
23
|
+
/**
|
|
24
|
+
* Shared logic: resolve repo, compute period, gather summary + git analysis, insert log.
|
|
25
|
+
* Used by both CLI and MCP. Returns the created log id or throws.
|
|
26
|
+
* Optional onProgress callback for CLI to show step-wise spinner (e.g. analyzing → pushing).
|
|
27
|
+
*/
|
|
28
|
+
export const buildAndPushActivityLog = async (timeFrame, cwd, onProgress) => {
|
|
29
|
+
const repoRoot = getRepoRoot(cwd);
|
|
30
|
+
if (!repoRoot) {
|
|
31
|
+
throw new Error('Not a git repository. Run from a project with git or pass a valid repo path.');
|
|
32
|
+
}
|
|
33
|
+
const period = getPeriodForTimeFrame(timeFrame);
|
|
34
|
+
return buildAndPushCore(repoRoot, period, timeFrame, onProgress);
|
|
35
|
+
};
|
|
32
36
|
/**
|
|
33
37
|
* Push an activity log for a custom date or date range. timeFrame is used for display (day/week/month).
|
|
34
38
|
*/
|
|
@@ -37,19 +41,7 @@ export const buildAndPushActivityLogWithPeriod = async (period, timeFrame, cwd,
|
|
|
37
41
|
if (!repoRoot) {
|
|
38
42
|
throw new Error('Not a git repository. Run from a project with git or pass a valid repo path.');
|
|
39
43
|
}
|
|
40
|
-
onProgress
|
|
41
|
-
const repo_summary = getRepoSummary(repoRoot);
|
|
42
|
-
const git_analysis = getGitAnalysis(repoRoot, period.since, period.period_end);
|
|
43
|
-
const payload = {
|
|
44
|
-
time_frame: timeFrame,
|
|
45
|
-
period_start: period.period_start,
|
|
46
|
-
period_end: period.period_end,
|
|
47
|
-
repo_summary: repo_summary,
|
|
48
|
-
git_analysis: git_analysis,
|
|
49
|
-
};
|
|
50
|
-
onProgress?.('pushing');
|
|
51
|
-
const created = await insertActivityLog(payload);
|
|
52
|
-
return { id: created.id };
|
|
44
|
+
return buildAndPushCore(repoRoot, period, timeFrame, onProgress);
|
|
53
45
|
};
|
|
54
46
|
const ISO_DATE = /^\d{4}-\d{2}-\d{2}$/;
|
|
55
47
|
const isDateArg = (s) => ISO_DATE.test(s.trim());
|
|
@@ -86,45 +78,28 @@ const timeFrameForRange = (start, end) => {
|
|
|
86
78
|
return 'week';
|
|
87
79
|
return 'month';
|
|
88
80
|
};
|
|
89
|
-
|
|
90
|
-
if (!
|
|
91
|
-
|
|
92
|
-
|
|
81
|
+
const runLogWithParsedDates = async (parsed, cwd, spinner, onProgress) => {
|
|
82
|
+
if (!parseDate(parsed.start)) {
|
|
83
|
+
spinner.stop();
|
|
84
|
+
throw new ValidationError(`Invalid date "${parsed.start}". Use YYYY-MM-DD (e.g. 2025-02-06).`);
|
|
93
85
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
console.error(chalk.red('Not a git repository. Run this command from a project with git.'));
|
|
98
|
-
process.exit(1);
|
|
86
|
+
if (parsed.end !== undefined && !parseDate(parsed.end)) {
|
|
87
|
+
spinner.stop();
|
|
88
|
+
throw new ValidationError(`Invalid date "${parsed.end}". Use YYYY-MM-DD (e.g. 2025-02-06).`);
|
|
99
89
|
}
|
|
100
|
-
const
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
if (!parseDate(parsed.start)) {
|
|
107
|
-
spinner.stop();
|
|
108
|
-
throw new ValidationError(`Invalid date "${parsed.start}". Use YYYY-MM-DD (e.g. 2025-02-06).`);
|
|
109
|
-
}
|
|
110
|
-
if (parsed.end !== undefined && !parseDate(parsed.end)) {
|
|
111
|
-
spinner.stop();
|
|
112
|
-
throw new ValidationError(`Invalid date "${parsed.end}". Use YYYY-MM-DD (e.g. 2025-02-06).`);
|
|
113
|
-
}
|
|
114
|
-
const period = getPeriodForCustomDates(parsed.start, parsed.end);
|
|
115
|
-
const timeFrame = parsed.end ? timeFrameForRange(parsed.start, parsed.end) : 'day';
|
|
116
|
-
try {
|
|
117
|
-
const result = await buildAndPushActivityLogWithPeriod(period, timeFrame, cwd, onProgress);
|
|
118
|
-
spinner.succeed(chalk.green('Log saved.'));
|
|
119
|
-
console.log(chalk.dim(result.id));
|
|
120
|
-
}
|
|
121
|
-
catch (err) {
|
|
122
|
-
spinner.fail(chalk.red(err instanceof Error ? err.message : 'Failed'));
|
|
123
|
-
throw err;
|
|
124
|
-
}
|
|
125
|
-
return;
|
|
90
|
+
const period = getPeriodForCustomDates(parsed.start, parsed.end);
|
|
91
|
+
const timeFrame = parsed.end ? timeFrameForRange(parsed.start, parsed.end) : 'day';
|
|
92
|
+
try {
|
|
93
|
+
const result = await buildAndPushActivityLogWithPeriod(period, timeFrame, cwd, onProgress);
|
|
94
|
+
spinner.succeed(chalk.green('Log saved.'));
|
|
95
|
+
console.log(chalk.dim(result.id));
|
|
126
96
|
}
|
|
127
|
-
|
|
97
|
+
catch (err) {
|
|
98
|
+
spinner.fail(chalk.red(err instanceof Error ? err.message : 'Failed'));
|
|
99
|
+
throw err;
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
const runLogInteractive = async (cwd, onProgress) => {
|
|
128
103
|
const choice = await inquirer.prompt([
|
|
129
104
|
{
|
|
130
105
|
name: 'time_frame',
|
|
@@ -137,10 +112,9 @@ export const runLog = async (args = []) => {
|
|
|
137
112
|
],
|
|
138
113
|
},
|
|
139
114
|
]);
|
|
140
|
-
const
|
|
141
|
-
spinner.start('Analyzing repository…');
|
|
115
|
+
const spinner = ora('Analyzing repository…').start();
|
|
142
116
|
try {
|
|
143
|
-
const result = await buildAndPushActivityLog(
|
|
117
|
+
const result = await buildAndPushActivityLog(choice.time_frame, cwd, onProgress);
|
|
144
118
|
spinner.succeed(chalk.green('Log saved.'));
|
|
145
119
|
console.log(chalk.dim(result.id));
|
|
146
120
|
}
|
|
@@ -149,3 +123,26 @@ export const runLog = async (args = []) => {
|
|
|
149
123
|
throw err;
|
|
150
124
|
}
|
|
151
125
|
};
|
|
126
|
+
export const runLog = async (args = []) => {
|
|
127
|
+
if (!loadCredentials()) {
|
|
128
|
+
console.error(chalk.red('Not logged in. Run bitcompass login.'));
|
|
129
|
+
process.exit(1);
|
|
130
|
+
}
|
|
131
|
+
const cwd = process.cwd();
|
|
132
|
+
const repoRoot = getRepoRoot(cwd);
|
|
133
|
+
if (!repoRoot) {
|
|
134
|
+
console.error(chalk.red('Not a git repository. Run this command from a project with git.'));
|
|
135
|
+
process.exit(1);
|
|
136
|
+
}
|
|
137
|
+
const parsed = parseLogArgs(args);
|
|
138
|
+
const spinner = ora('Analyzing repository…').start();
|
|
139
|
+
const onProgress = (step) => {
|
|
140
|
+
spinner.text = step === 'analyzing' ? 'Analyzing repository…' : 'Pushing activity log…';
|
|
141
|
+
};
|
|
142
|
+
if (parsed) {
|
|
143
|
+
await runLogWithParsedDates(parsed, cwd, spinner, onProgress);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
spinner.stop();
|
|
147
|
+
await runLogInteractive(cwd, onProgress);
|
|
148
|
+
};
|
package/dist/commands/login.js
CHANGED
|
@@ -6,6 +6,10 @@ 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
|
+
/** Hardcoded Compass rules URL: prod or local testing. Used for "See rules on Compass" link after login. */
|
|
10
|
+
const COMPASS_RULES_URL_PROD = 'https://bitcompass.vercel.app/rules';
|
|
11
|
+
const COMPASS_RULES_URL_DEV = 'http://localhost:8080/rules';
|
|
12
|
+
const getCompassRulesUrl = () => process.env.NODE_ENV === 'development' ? COMPASS_RULES_URL_DEV : COMPASS_RULES_URL_PROD;
|
|
9
13
|
/** Design tokens matching src/index.css (light theme) */
|
|
10
14
|
const STYLES = {
|
|
11
15
|
background: 'hsl(0, 0%, 98%)',
|
|
@@ -18,7 +22,25 @@ const STYLES = {
|
|
|
18
22
|
radius: '0.625rem',
|
|
19
23
|
shadow: '0 10px 15px -3px hsl(220 13% 11% / 0.08), 0 4px 6px -4px hsl(220 13% 11% / 0.05)',
|
|
20
24
|
};
|
|
21
|
-
|
|
25
|
+
/**
|
|
26
|
+
* Builds the Compass rules URL, optionally with auth hash so the web app can set the session
|
|
27
|
+
* without asking the user to log in again (use-auth.ts reads #access_token and #refresh_token).
|
|
28
|
+
*/
|
|
29
|
+
const compassRulesUrlWithSession = (baseUrl, session) => {
|
|
30
|
+
if (!session?.access_token || !session?.refresh_token)
|
|
31
|
+
return baseUrl;
|
|
32
|
+
const hash = `access_token=${encodeURIComponent(session.access_token)}&refresh_token=${encodeURIComponent(session.refresh_token)}`;
|
|
33
|
+
return `${baseUrl}#${hash}`;
|
|
34
|
+
};
|
|
35
|
+
/** Builds the post-login success (recap) page HTML with Compass rules CTA link. Session in URL hash persists login on the website. */
|
|
36
|
+
const buildCallbackSuccessHtml = (compassRulesUrl, session) => {
|
|
37
|
+
const href = compassRulesUrlWithSession(compassRulesUrl, session);
|
|
38
|
+
const compassCtaBlock = `
|
|
39
|
+
<div class="compass-cta-block">
|
|
40
|
+
<a href="${escapeHtml(href)}" class="compass-cta" target="_blank" rel="noopener noreferrer">See Available Rules on Compass</a>
|
|
41
|
+
<p class="compass-cta-hint">Opens in the same browser; you’ll be signed in automatically.</p>
|
|
42
|
+
</div>`;
|
|
43
|
+
return `<!DOCTYPE html>
|
|
22
44
|
<html lang="en">
|
|
23
45
|
<head>
|
|
24
46
|
<meta charset="UTF-8" />
|
|
@@ -119,6 +141,28 @@ const CALLBACK_SUCCESS_HTML = `<!DOCTYPE html>
|
|
|
119
141
|
}
|
|
120
142
|
.copy-btn:hover { opacity: 0.9; }
|
|
121
143
|
.copy-btn.copied { background: ${STYLES.muted}; cursor: default; }
|
|
144
|
+
.compass-cta-block {
|
|
145
|
+
margin-top: 1.25rem;
|
|
146
|
+
padding-top: 1.25rem;
|
|
147
|
+
border-top: 1px solid ${STYLES.border};
|
|
148
|
+
}
|
|
149
|
+
.compass-cta {
|
|
150
|
+
display: inline-block;
|
|
151
|
+
padding: 0.5rem 1rem;
|
|
152
|
+
background: ${STYLES.primary};
|
|
153
|
+
color: ${STYLES.primaryForeground};
|
|
154
|
+
font-size: 0.875rem;
|
|
155
|
+
font-weight: 600;
|
|
156
|
+
text-decoration: none;
|
|
157
|
+
border-radius: 0.5rem;
|
|
158
|
+
transition: opacity 0.15s;
|
|
159
|
+
}
|
|
160
|
+
.compass-cta:hover { opacity: 0.9; }
|
|
161
|
+
.compass-cta-hint {
|
|
162
|
+
margin: 0.5rem 0 0;
|
|
163
|
+
font-size: 0.75rem;
|
|
164
|
+
color: ${STYLES.muted};
|
|
165
|
+
}
|
|
122
166
|
</style>
|
|
123
167
|
</head>
|
|
124
168
|
<body>
|
|
@@ -131,7 +175,7 @@ const CALLBACK_SUCCESS_HTML = `<!DOCTYPE html>
|
|
|
131
175
|
</div>
|
|
132
176
|
<h1>You're all set</h1>
|
|
133
177
|
<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>
|
|
134
|
-
<p class="hint">Return to your terminal to continue.</p
|
|
178
|
+
<p class="hint">Return to your terminal to continue.</p>${compassCtaBlock}
|
|
135
179
|
<div class="verify-block">
|
|
136
180
|
<p class="muted" style="margin:0">Verify in terminal:</p>
|
|
137
181
|
<div class="cmd-row">
|
|
@@ -159,6 +203,7 @@ const CALLBACK_SUCCESS_HTML = `<!DOCTYPE html>
|
|
|
159
203
|
</script>
|
|
160
204
|
</body>
|
|
161
205
|
</html>`;
|
|
206
|
+
};
|
|
162
207
|
const escapeHtml = (s) => s
|
|
163
208
|
.replace(/&/g, '&')
|
|
164
209
|
.replace(/</g, '<')
|
|
@@ -301,7 +346,7 @@ export const runLogin = async () => {
|
|
|
301
346
|
const tokenPath = getTokenFilePath();
|
|
302
347
|
saveCredentials(creds);
|
|
303
348
|
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
304
|
-
res.end(
|
|
349
|
+
res.end(buildCallbackSuccessHtml(getCompassRulesUrl(), session));
|
|
305
350
|
spinner.succeed(chalk.green('Logged in successfully.'));
|
|
306
351
|
console.log(chalk.dim('Credentials saved to:'), tokenPath);
|
|
307
352
|
server.close();
|
package/dist/commands/rules.d.ts
CHANGED
|
@@ -8,4 +8,6 @@ export declare const runRulesPull: (id?: string, options?: {
|
|
|
8
8
|
global?: boolean;
|
|
9
9
|
copy?: boolean;
|
|
10
10
|
}) => Promise<void>;
|
|
11
|
-
export declare const runRulesPush: (file?: string
|
|
11
|
+
export declare const runRulesPush: (file?: string, options?: {
|
|
12
|
+
projectId?: string;
|
|
13
|
+
}) => Promise<void>;
|
package/dist/commands/rules.js
CHANGED
|
@@ -2,10 +2,10 @@ import inquirer from 'inquirer';
|
|
|
2
2
|
import ora from 'ora';
|
|
3
3
|
import chalk from 'chalk';
|
|
4
4
|
import { loadCredentials } from '../auth/config.js';
|
|
5
|
-
import { searchRules, fetchRules, getRuleById
|
|
5
|
+
import { searchRules, fetchRules, getRuleById } from '../api/client.js';
|
|
6
6
|
import { pullRuleToFile } from '../lib/rule-file-ops.js';
|
|
7
|
-
import { parseRuleMdcContent } from '../lib/mdc-format.js';
|
|
8
7
|
import { formatList, shouldUseTable } from '../lib/list-format.js';
|
|
8
|
+
import { runSharePush } from './share.js';
|
|
9
9
|
export const runRulesSearch = async (query, options) => {
|
|
10
10
|
if (!loadCredentials()) {
|
|
11
11
|
console.error(chalk.red('Not logged in. Run bitcompass login.'));
|
|
@@ -96,48 +96,6 @@ export const runRulesPull = async (id, options) => {
|
|
|
96
96
|
process.exit(message.includes('not found') ? 2 : 1);
|
|
97
97
|
}
|
|
98
98
|
};
|
|
99
|
-
export const runRulesPush = async (file) => {
|
|
100
|
-
|
|
101
|
-
console.error(chalk.red('Not logged in. Run bitcompass login.'));
|
|
102
|
-
process.exit(1);
|
|
103
|
-
}
|
|
104
|
-
let payload;
|
|
105
|
-
if (file) {
|
|
106
|
-
const { readFileSync } = await import('fs');
|
|
107
|
-
const raw = readFileSync(file, 'utf-8');
|
|
108
|
-
try {
|
|
109
|
-
payload = JSON.parse(raw);
|
|
110
|
-
}
|
|
111
|
-
catch {
|
|
112
|
-
const parsed = parseRuleMdcContent(raw);
|
|
113
|
-
if (parsed) {
|
|
114
|
-
const titleFromBody = parsed.body.split('\n')[0]?.replace(/^#\s*/, '').trim() || 'Untitled';
|
|
115
|
-
payload = {
|
|
116
|
-
kind: 'rule',
|
|
117
|
-
title: titleFromBody,
|
|
118
|
-
description: parsed.description,
|
|
119
|
-
body: parsed.body,
|
|
120
|
-
globs: parsed.globs ?? undefined,
|
|
121
|
-
always_apply: parsed.alwaysApply,
|
|
122
|
-
};
|
|
123
|
-
}
|
|
124
|
-
else {
|
|
125
|
-
const lines = raw.split('\n');
|
|
126
|
-
const title = lines[0].replace(/^#\s*/, '') || 'Untitled';
|
|
127
|
-
payload = { kind: 'rule', title, description: '', body: raw };
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
else {
|
|
132
|
-
const answers = await inquirer.prompt([
|
|
133
|
-
{ name: 'title', message: 'Rule title', type: 'input', default: 'Untitled' },
|
|
134
|
-
{ name: 'description', message: 'Description', type: 'input', default: '' },
|
|
135
|
-
{ name: 'body', message: 'Rule content', type: 'editor', default: '' },
|
|
136
|
-
]);
|
|
137
|
-
payload = { kind: 'rule', title: answers.title, description: answers.description, body: answers.body };
|
|
138
|
-
}
|
|
139
|
-
const spinner = ora('Publishing rule…').start();
|
|
140
|
-
const created = await insertRule(payload);
|
|
141
|
-
spinner.succeed(chalk.green('Published rule ') + created.id);
|
|
142
|
-
console.log(chalk.dim(created.title));
|
|
99
|
+
export const runRulesPush = async (file, options) => {
|
|
100
|
+
await runSharePush(file, { kind: 'rule', projectId: options?.projectId });
|
|
143
101
|
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { RuleInsert, RuleKind } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Infers RuleKind from file content (frontmatter kind) or filename prefix.
|
|
4
|
+
*/
|
|
5
|
+
export declare const inferKindFromFile: (filePath: string, rawContent: string) => RuleKind | null;
|
|
6
|
+
/**
|
|
7
|
+
* Parses file content into RuleInsert. Uses explicitKind if provided, else inferred from file.
|
|
8
|
+
* If kind cannot be determined, payload.kind may be left undefined (caller should prompt).
|
|
9
|
+
*/
|
|
10
|
+
export declare const parseFileToPayload: (filePath: string, rawContent: string, explicitKind?: RuleKind) => Omit<RuleInsert, "kind"> & {
|
|
11
|
+
kind?: RuleKind;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Unified share push: optionally read from file, infer or prompt for kind, then publish.
|
|
15
|
+
*/
|
|
16
|
+
export declare const runSharePush: (file?: string, options?: {
|
|
17
|
+
kind?: RuleKind;
|
|
18
|
+
projectId?: string;
|
|
19
|
+
}) => Promise<void>;
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { readFileSync } from 'fs';
|
|
2
|
+
import inquirer from 'inquirer';
|
|
3
|
+
import ora from 'ora';
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
import { loadCredentials } from '../auth/config.js';
|
|
6
|
+
import { insertRule } from '../api/client.js';
|
|
7
|
+
import { parseRuleMdcContent, parseFrontmatterKind } from '../lib/mdc-format.js';
|
|
8
|
+
import { SHARE_KIND_CHOICES, inferKindFromFilename } from '../lib/share-types.js';
|
|
9
|
+
const KIND_LABELS = {
|
|
10
|
+
rule: 'rule',
|
|
11
|
+
solution: 'solution',
|
|
12
|
+
skill: 'skill',
|
|
13
|
+
command: 'command',
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Infers RuleKind from file content (frontmatter kind) or filename prefix.
|
|
17
|
+
*/
|
|
18
|
+
export const inferKindFromFile = (filePath, rawContent) => {
|
|
19
|
+
const fromFrontmatter = parseFrontmatterKind(rawContent);
|
|
20
|
+
if (fromFrontmatter)
|
|
21
|
+
return fromFrontmatter;
|
|
22
|
+
return inferKindFromFilename(filePath);
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Parses file content into RuleInsert. Uses explicitKind if provided, else inferred from file.
|
|
26
|
+
* If kind cannot be determined, payload.kind may be left undefined (caller should prompt).
|
|
27
|
+
*/
|
|
28
|
+
export const parseFileToPayload = (filePath, rawContent, explicitKind) => {
|
|
29
|
+
const inferredKind = inferKindFromFile(filePath, rawContent);
|
|
30
|
+
const kind = explicitKind ?? inferredKind ?? undefined;
|
|
31
|
+
try {
|
|
32
|
+
const parsed = JSON.parse(rawContent);
|
|
33
|
+
const payload = {
|
|
34
|
+
kind: parsed.kind ?? kind ?? 'rule',
|
|
35
|
+
title: parsed.title ?? 'Untitled',
|
|
36
|
+
description: parsed.description ?? '',
|
|
37
|
+
body: parsed.body ?? '',
|
|
38
|
+
context: typeof parsed.context === 'string' ? parsed.context : undefined,
|
|
39
|
+
examples: Array.isArray(parsed.examples) ? parsed.examples : undefined,
|
|
40
|
+
technologies: Array.isArray(parsed.technologies) ? parsed.technologies : undefined,
|
|
41
|
+
globs: typeof parsed.globs === 'string' ? parsed.globs : undefined,
|
|
42
|
+
always_apply: Boolean(parsed.always_apply),
|
|
43
|
+
};
|
|
44
|
+
if (explicitKind)
|
|
45
|
+
payload.kind = explicitKind;
|
|
46
|
+
else if (kind)
|
|
47
|
+
payload.kind = kind;
|
|
48
|
+
return payload;
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
// not JSON
|
|
52
|
+
}
|
|
53
|
+
const mdc = parseRuleMdcContent(rawContent);
|
|
54
|
+
if (mdc) {
|
|
55
|
+
const titleFromBody = mdc.body.split('\n')[0]?.replace(/^#\s*/, '').trim() || 'Untitled';
|
|
56
|
+
return {
|
|
57
|
+
kind: explicitKind ?? mdc.kind ?? inferredKind ?? undefined,
|
|
58
|
+
title: titleFromBody,
|
|
59
|
+
description: mdc.description,
|
|
60
|
+
body: mdc.body,
|
|
61
|
+
globs: mdc.globs ?? undefined,
|
|
62
|
+
always_apply: mdc.alwaysApply,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
const lines = rawContent.split('\n');
|
|
66
|
+
const title = (lines[0] ?? '').replace(/^#\s*/, '').trim() || 'Untitled';
|
|
67
|
+
return {
|
|
68
|
+
kind: explicitKind ?? inferredKind ?? undefined,
|
|
69
|
+
title,
|
|
70
|
+
description: '',
|
|
71
|
+
body: rawContent,
|
|
72
|
+
};
|
|
73
|
+
};
|
|
74
|
+
const promptForKind = async () => {
|
|
75
|
+
const { kind } = await inquirer.prompt([
|
|
76
|
+
{
|
|
77
|
+
name: 'kind',
|
|
78
|
+
message: 'What are you sharing?',
|
|
79
|
+
type: 'list',
|
|
80
|
+
choices: SHARE_KIND_CHOICES.map((c) => ({ name: `${c.name} – ${c.description}`, value: c.value })),
|
|
81
|
+
},
|
|
82
|
+
]);
|
|
83
|
+
return kind;
|
|
84
|
+
};
|
|
85
|
+
/**
|
|
86
|
+
* Unified share push: optionally read from file, infer or prompt for kind, then publish.
|
|
87
|
+
*/
|
|
88
|
+
export const runSharePush = async (file, options) => {
|
|
89
|
+
if (!loadCredentials()) {
|
|
90
|
+
console.error(chalk.red('Not logged in. Run bitcompass login.'));
|
|
91
|
+
process.exit(1);
|
|
92
|
+
}
|
|
93
|
+
let payload;
|
|
94
|
+
if (file) {
|
|
95
|
+
const raw = readFileSync(file, 'utf-8');
|
|
96
|
+
const parsed = parseFileToPayload(file, raw, options?.kind);
|
|
97
|
+
let kind = parsed.kind;
|
|
98
|
+
if (!kind) {
|
|
99
|
+
kind = await promptForKind();
|
|
100
|
+
}
|
|
101
|
+
payload = {
|
|
102
|
+
kind,
|
|
103
|
+
title: parsed.title,
|
|
104
|
+
description: parsed.description ?? '',
|
|
105
|
+
body: parsed.body,
|
|
106
|
+
context: parsed.context,
|
|
107
|
+
examples: parsed.examples,
|
|
108
|
+
technologies: parsed.technologies,
|
|
109
|
+
project_id: options?.projectId,
|
|
110
|
+
globs: parsed.globs,
|
|
111
|
+
always_apply: parsed.always_apply,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
const kind = options?.kind ?? (await promptForKind());
|
|
116
|
+
const answers = await inquirer.prompt([
|
|
117
|
+
{ name: 'title', message: 'Title', type: 'input', default: 'Untitled' },
|
|
118
|
+
{ name: 'description', message: 'Description', type: 'input', default: '' },
|
|
119
|
+
{ name: 'body', message: 'Content', type: 'editor', default: '' },
|
|
120
|
+
]);
|
|
121
|
+
payload = {
|
|
122
|
+
kind,
|
|
123
|
+
title: answers.title,
|
|
124
|
+
description: answers.description,
|
|
125
|
+
body: answers.body,
|
|
126
|
+
project_id: options?.projectId,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
if (options?.projectId) {
|
|
130
|
+
payload = { ...payload, project_id: options.projectId };
|
|
131
|
+
}
|
|
132
|
+
const label = KIND_LABELS[payload.kind];
|
|
133
|
+
const spinner = ora(`Publishing ${label}…`).start();
|
|
134
|
+
const created = await insertRule(payload);
|
|
135
|
+
spinner.succeed(chalk.green(`Published ${label} `) + created.id);
|
|
136
|
+
console.log(chalk.dim(created.title));
|
|
137
|
+
};
|
|
@@ -8,4 +8,6 @@ export declare const runSkillsPull: (id?: string, options?: {
|
|
|
8
8
|
global?: boolean;
|
|
9
9
|
copy?: boolean;
|
|
10
10
|
}) => Promise<void>;
|
|
11
|
-
export declare const runSkillsPush: (file?: string
|
|
11
|
+
export declare const runSkillsPush: (file?: string, options?: {
|
|
12
|
+
projectId?: string;
|
|
13
|
+
}) => Promise<void>;
|
package/dist/commands/skills.js
CHANGED
|
@@ -2,9 +2,10 @@ import inquirer from 'inquirer';
|
|
|
2
2
|
import ora from 'ora';
|
|
3
3
|
import chalk from 'chalk';
|
|
4
4
|
import { loadCredentials } from '../auth/config.js';
|
|
5
|
-
import { searchRules, fetchRules, getRuleById
|
|
5
|
+
import { searchRules, fetchRules, getRuleById } from '../api/client.js';
|
|
6
6
|
import { pullRuleToFile } from '../lib/rule-file-ops.js';
|
|
7
7
|
import { formatList, shouldUseTable } from '../lib/list-format.js';
|
|
8
|
+
import { runSharePush } from './share.js';
|
|
8
9
|
export const runSkillsSearch = async (query, options) => {
|
|
9
10
|
if (!loadCredentials()) {
|
|
10
11
|
console.error(chalk.red('Not logged in. Run bitcompass login.'));
|
|
@@ -95,35 +96,6 @@ export const runSkillsPull = async (id, options) => {
|
|
|
95
96
|
process.exit(message.includes('not found') ? 2 : 1);
|
|
96
97
|
}
|
|
97
98
|
};
|
|
98
|
-
export const runSkillsPush = async (file) => {
|
|
99
|
-
|
|
100
|
-
console.error(chalk.red('Not logged in. Run bitcompass login.'));
|
|
101
|
-
process.exit(1);
|
|
102
|
-
}
|
|
103
|
-
let payload;
|
|
104
|
-
if (file) {
|
|
105
|
-
const { readFileSync } = await import('fs');
|
|
106
|
-
const raw = readFileSync(file, 'utf-8');
|
|
107
|
-
try {
|
|
108
|
-
payload = JSON.parse(raw);
|
|
109
|
-
payload.kind = 'skill';
|
|
110
|
-
}
|
|
111
|
-
catch {
|
|
112
|
-
const lines = raw.split('\n');
|
|
113
|
-
const title = lines[0].replace(/^#\s*/, '') || 'Untitled';
|
|
114
|
-
payload = { kind: 'skill', title, description: '', body: raw };
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
else {
|
|
118
|
-
const answers = await inquirer.prompt([
|
|
119
|
-
{ name: 'title', message: 'Skill title', type: 'input', default: 'Untitled' },
|
|
120
|
-
{ name: 'description', message: 'Description', type: 'input', default: '' },
|
|
121
|
-
{ name: 'body', message: 'Skill content', type: 'editor', default: '' },
|
|
122
|
-
]);
|
|
123
|
-
payload = { kind: 'skill', title: answers.title, description: answers.description, body: answers.body };
|
|
124
|
-
}
|
|
125
|
-
const spinner = ora('Publishing skill…').start();
|
|
126
|
-
const created = await insertRule(payload);
|
|
127
|
-
spinner.succeed(chalk.green('Published skill ') + created.id);
|
|
128
|
-
console.log(chalk.dim(created.title));
|
|
99
|
+
export const runSkillsPush = async (file, options) => {
|
|
100
|
+
await runSharePush(file, { kind: 'skill', projectId: options?.projectId });
|
|
129
101
|
};
|
|
@@ -8,4 +8,6 @@ export declare const runSolutionsPull: (id?: string, options?: {
|
|
|
8
8
|
global?: boolean;
|
|
9
9
|
copy?: boolean;
|
|
10
10
|
}) => Promise<void>;
|
|
11
|
-
export declare const runSolutionsPush: (file?: string
|
|
11
|
+
export declare const runSolutionsPush: (file?: string, options?: {
|
|
12
|
+
projectId?: string;
|
|
13
|
+
}) => Promise<void>;
|
|
@@ -2,9 +2,10 @@ import inquirer from 'inquirer';
|
|
|
2
2
|
import ora from 'ora';
|
|
3
3
|
import chalk from 'chalk';
|
|
4
4
|
import { loadCredentials } from '../auth/config.js';
|
|
5
|
-
import { searchRules, fetchRules, getRuleById
|
|
5
|
+
import { searchRules, fetchRules, getRuleById } from '../api/client.js';
|
|
6
6
|
import { pullRuleToFile } from '../lib/rule-file-ops.js';
|
|
7
7
|
import { formatList, shouldUseTable } from '../lib/list-format.js';
|
|
8
|
+
import { runSharePush } from './share.js';
|
|
8
9
|
export const runSolutionsSearch = async (query, options) => {
|
|
9
10
|
if (!loadCredentials()) {
|
|
10
11
|
console.error(chalk.red('Not logged in. Run bitcompass login.'));
|
|
@@ -95,35 +96,6 @@ export const runSolutionsPull = async (id, options) => {
|
|
|
95
96
|
process.exit(message.includes('not found') ? 2 : 1);
|
|
96
97
|
}
|
|
97
98
|
};
|
|
98
|
-
export const runSolutionsPush = async (file) => {
|
|
99
|
-
|
|
100
|
-
console.error(chalk.red('Not logged in. Run bitcompass login.'));
|
|
101
|
-
process.exit(1);
|
|
102
|
-
}
|
|
103
|
-
let payload;
|
|
104
|
-
if (file) {
|
|
105
|
-
const { readFileSync } = await import('fs');
|
|
106
|
-
const raw = readFileSync(file, 'utf-8');
|
|
107
|
-
try {
|
|
108
|
-
payload = JSON.parse(raw);
|
|
109
|
-
payload.kind = 'solution';
|
|
110
|
-
}
|
|
111
|
-
catch {
|
|
112
|
-
const lines = raw.split('\n');
|
|
113
|
-
const title = lines[0].replace(/^#\s*/, '') || 'Untitled';
|
|
114
|
-
payload = { kind: 'solution', title, description: '', body: raw };
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
else {
|
|
118
|
-
const answers = await inquirer.prompt([
|
|
119
|
-
{ name: 'title', message: 'Problem title', type: 'input', default: 'Untitled' },
|
|
120
|
-
{ name: 'description', message: 'Description', type: 'input', default: '' },
|
|
121
|
-
{ name: 'body', message: 'Solution content', type: 'editor', default: '' },
|
|
122
|
-
]);
|
|
123
|
-
payload = { kind: 'solution', title: answers.title, description: answers.description, body: answers.body };
|
|
124
|
-
}
|
|
125
|
-
const spinner = ora('Publishing solution…').start();
|
|
126
|
-
const created = await insertRule(payload);
|
|
127
|
-
spinner.succeed(chalk.green('Published solution ') + created.id);
|
|
128
|
-
console.log(chalk.dim(created.title));
|
|
99
|
+
export const runSolutionsPush = async (file, options) => {
|
|
100
|
+
await runSharePush(file, { kind: 'solution', projectId: options?.projectId });
|
|
129
101
|
};
|
package/dist/index.js
CHANGED
|
@@ -16,6 +16,7 @@ import { runSolutionsList, runSolutionsPull, runSolutionsPush, runSolutionsSearc
|
|
|
16
16
|
import { runSkillsList, runSkillsPull, runSkillsPush, runSkillsSearch } from './commands/skills.js';
|
|
17
17
|
import { runCommandsList, runCommandsPull, runCommandsPush, runCommandsSearch } from './commands/commands.js';
|
|
18
18
|
import { runGlossary } from './commands/glossary.js';
|
|
19
|
+
import { runSharePush } from './commands/share.js';
|
|
19
20
|
import { runWhoami } from './commands/whoami.js';
|
|
20
21
|
// Disable chalk colors when NO_COLOR is set or --no-color is passed (must run before any command)
|
|
21
22
|
if (process.env.NO_COLOR !== undefined || process.argv.includes('--no-color')) {
|
|
@@ -48,6 +49,25 @@ program
|
|
|
48
49
|
.command('glossary')
|
|
49
50
|
.description('Show glossary (rules, solutions, skills, commands)')
|
|
50
51
|
.action(runGlossary);
|
|
52
|
+
program
|
|
53
|
+
.command('share [file]')
|
|
54
|
+
.description('Share a rule, solution, skill, or command (prompts for type if not in file or --kind)')
|
|
55
|
+
.option('-k, --kind <kind>', 'Type: rule, solution, skill, or command (skips type prompt)')
|
|
56
|
+
.option('--project-id <uuid>', 'Scope to Compass project (UUID)')
|
|
57
|
+
.addHelpText('after', `
|
|
58
|
+
Examples:
|
|
59
|
+
bitcompass share
|
|
60
|
+
bitcompass share ./my-rule.mdc
|
|
61
|
+
bitcompass share ./doc.md --kind solution
|
|
62
|
+
`)
|
|
63
|
+
.action((file, opts) => {
|
|
64
|
+
const kind = opts?.kind;
|
|
65
|
+
if (opts?.kind && kind !== 'rule' && kind !== 'solution' && kind !== 'skill' && kind !== 'command') {
|
|
66
|
+
console.error(chalk.red('--kind must be one of: rule, solution, skill, command'));
|
|
67
|
+
process.exit(1);
|
|
68
|
+
}
|
|
69
|
+
return runSharePush(file, { kind, projectId: opts?.projectId }).catch(handleErr);
|
|
70
|
+
});
|
|
51
71
|
program
|
|
52
72
|
.command('init')
|
|
53
73
|
.description('Configure project: editor/AI provider and output folder for rules/docs/commands')
|
|
@@ -93,7 +113,11 @@ rules
|
|
|
93
113
|
.option('--copy', 'Copy file instead of creating symbolic link')
|
|
94
114
|
.addHelpText('after', '\nExamples:\n bitcompass rules pull <id>\n bitcompass rules pull <id> --global\n bitcompass rules pull <id> --copy\n')
|
|
95
115
|
.action((id, options) => runRulesPull(id, options).catch(handleErr));
|
|
96
|
-
rules
|
|
116
|
+
rules
|
|
117
|
+
.command('push [file]')
|
|
118
|
+
.description('Push a rule (file or interactive)')
|
|
119
|
+
.option('--project-id <uuid>', 'Scope to Compass project (UUID)')
|
|
120
|
+
.action((file, opts) => runRulesPush(file, { projectId: opts?.projectId }).catch(handleErr));
|
|
97
121
|
// solutions
|
|
98
122
|
const solutions = program.command('solutions').description('Manage solutions');
|
|
99
123
|
solutions
|
|
@@ -114,7 +138,11 @@ solutions
|
|
|
114
138
|
.option('--copy', 'Copy file instead of creating symbolic link')
|
|
115
139
|
.addHelpText('after', '\nExamples:\n bitcompass solutions pull <id>\n bitcompass solutions pull <id> --global\n')
|
|
116
140
|
.action((id, options) => runSolutionsPull(id, options).catch(handleErr));
|
|
117
|
-
solutions
|
|
141
|
+
solutions
|
|
142
|
+
.command('push [file]')
|
|
143
|
+
.description('Push a solution (file or interactive)')
|
|
144
|
+
.option('--project-id <uuid>', 'Scope to Compass project (UUID)')
|
|
145
|
+
.action((file, opts) => runSolutionsPush(file, { projectId: opts?.projectId }).catch(handleErr));
|
|
118
146
|
// skills
|
|
119
147
|
const skills = program.command('skills').description('Manage skills');
|
|
120
148
|
skills
|
|
@@ -135,7 +163,11 @@ skills
|
|
|
135
163
|
.option('--copy', 'Copy file instead of creating symbolic link')
|
|
136
164
|
.addHelpText('after', '\nExamples:\n bitcompass skills pull <id>\n bitcompass skills pull <id> --global\n')
|
|
137
165
|
.action((id, options) => runSkillsPull(id, options).catch(handleErr));
|
|
138
|
-
skills
|
|
166
|
+
skills
|
|
167
|
+
.command('push [file]')
|
|
168
|
+
.description('Push a skill (file or interactive)')
|
|
169
|
+
.option('--project-id <uuid>', 'Scope to Compass project (UUID)')
|
|
170
|
+
.action((file, opts) => runSkillsPush(file, { projectId: opts?.projectId }).catch(handleErr));
|
|
139
171
|
// commands
|
|
140
172
|
const commands = program.command('commands').description('Manage commands');
|
|
141
173
|
commands
|
|
@@ -156,7 +188,11 @@ commands
|
|
|
156
188
|
.option('--copy', 'Copy file instead of creating symbolic link')
|
|
157
189
|
.addHelpText('after', '\nExamples:\n bitcompass commands pull <id>\n bitcompass commands pull <id> --global\n')
|
|
158
190
|
.action((id, options) => runCommandsPull(id, options).catch(handleErr));
|
|
159
|
-
commands
|
|
191
|
+
commands
|
|
192
|
+
.command('push [file]')
|
|
193
|
+
.description('Push a command (file or interactive)')
|
|
194
|
+
.option('--project-id <uuid>', 'Scope to Compass project (UUID)')
|
|
195
|
+
.action((file, opts) => runCommandsPush(file, { projectId: opts?.projectId }).catch(handleErr));
|
|
160
196
|
// mcp
|
|
161
197
|
const mcp = program.command('mcp').description('MCP server');
|
|
162
198
|
mcp.command('start').description('Start MCP server (stdio)').action(() => runMcpStart().catch(handleErr));
|
package/dist/lib/mdc-format.d.ts
CHANGED
|
@@ -1,12 +1,18 @@
|
|
|
1
|
-
import type { Rule } from '../types.js';
|
|
1
|
+
import type { Rule, RuleKind } from '../types.js';
|
|
2
2
|
/**
|
|
3
|
-
* Builds Cursor .mdc content for a rule: YAML frontmatter (description, globs, alwaysApply) then body.
|
|
3
|
+
* Builds Cursor .mdc content for a rule: YAML frontmatter (description, globs, alwaysApply, kind) then body.
|
|
4
4
|
*/
|
|
5
5
|
export declare const buildRuleMdcContent: (rule: Rule) => string;
|
|
6
|
+
/**
|
|
7
|
+
* Builds .md content for solution, skill, or command with frontmatter (kind, description)
|
|
8
|
+
* so that bitcompass share can infer kind when re-pushing.
|
|
9
|
+
*/
|
|
10
|
+
export declare const buildMarkdownWithKind: (rule: Rule) => string;
|
|
6
11
|
export interface ParsedMdcFrontmatter {
|
|
7
12
|
description: string;
|
|
8
13
|
globs?: string;
|
|
9
14
|
alwaysApply: boolean;
|
|
15
|
+
kind?: RuleKind;
|
|
10
16
|
body: string;
|
|
11
17
|
}
|
|
12
18
|
/**
|
|
@@ -14,3 +20,8 @@ export interface ParsedMdcFrontmatter {
|
|
|
14
20
|
* Returns null if the file does not start with --- (not frontmatter format).
|
|
15
21
|
*/
|
|
16
22
|
export declare const parseRuleMdcContent: (raw: string) => ParsedMdcFrontmatter | null;
|
|
23
|
+
/**
|
|
24
|
+
* Returns kind from the first frontmatter block if present and valid.
|
|
25
|
+
* Use for .md or .mdc files when you only need kind (e.g. solution/skill/command round-trip).
|
|
26
|
+
*/
|
|
27
|
+
export declare const parseFrontmatterKind: (raw: string) => RuleKind | null;
|
package/dist/lib/mdc-format.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
import { isValidRuleKind } from './share-types.js';
|
|
1
2
|
const FRONTMATTER_DELIM = '---';
|
|
2
3
|
/**
|
|
3
|
-
* Builds Cursor .mdc content for a rule: YAML frontmatter (description, globs, alwaysApply) then body.
|
|
4
|
+
* Builds Cursor .mdc content for a rule: YAML frontmatter (description, globs, alwaysApply, kind) then body.
|
|
4
5
|
*/
|
|
5
6
|
export const buildRuleMdcContent = (rule) => {
|
|
6
7
|
const lines = [FRONTMATTER_DELIM];
|
|
8
|
+
lines.push(`kind: ${rule.kind}`);
|
|
7
9
|
lines.push(`description: ${escapeYamlValue(rule.description ?? '')}`);
|
|
8
10
|
if (rule.globs != null && String(rule.globs).trim() !== '') {
|
|
9
11
|
lines.push(`globs: ${escapeYamlValue(String(rule.globs).trim())}`);
|
|
@@ -17,6 +19,35 @@ export const buildRuleMdcContent = (rule) => {
|
|
|
17
19
|
}
|
|
18
20
|
return lines.join('\n');
|
|
19
21
|
};
|
|
22
|
+
/**
|
|
23
|
+
* Builds .md content for solution, skill, or command with frontmatter (kind, description)
|
|
24
|
+
* so that bitcompass share can infer kind when re-pushing.
|
|
25
|
+
*/
|
|
26
|
+
export const buildMarkdownWithKind = (rule) => {
|
|
27
|
+
if (rule.kind === 'rule') {
|
|
28
|
+
return buildRuleMdcContent(rule);
|
|
29
|
+
}
|
|
30
|
+
const lines = [FRONTMATTER_DELIM];
|
|
31
|
+
lines.push(`kind: ${rule.kind}`);
|
|
32
|
+
lines.push(`description: ${escapeYamlValue(rule.description ?? '')}`);
|
|
33
|
+
lines.push(FRONTMATTER_DELIM);
|
|
34
|
+
lines.push('');
|
|
35
|
+
lines.push(`# ${rule.title}`);
|
|
36
|
+
lines.push('');
|
|
37
|
+
if (rule.description) {
|
|
38
|
+
lines.push(rule.description);
|
|
39
|
+
lines.push('');
|
|
40
|
+
}
|
|
41
|
+
if (rule.kind === 'solution') {
|
|
42
|
+
lines.push('## Solution');
|
|
43
|
+
lines.push('');
|
|
44
|
+
}
|
|
45
|
+
lines.push(rule.body.trimEnd());
|
|
46
|
+
if (!rule.body.endsWith('\n')) {
|
|
47
|
+
lines.push('');
|
|
48
|
+
}
|
|
49
|
+
return lines.join('\n');
|
|
50
|
+
};
|
|
20
51
|
const escapeYamlValue = (s) => {
|
|
21
52
|
if (/^[a-z0-9-]+$/i.test(s) && !s.includes(':'))
|
|
22
53
|
return s;
|
|
@@ -41,6 +72,7 @@ export const parseRuleMdcContent = (raw) => {
|
|
|
41
72
|
let description = '';
|
|
42
73
|
let globs;
|
|
43
74
|
let alwaysApply = false;
|
|
75
|
+
let kind;
|
|
44
76
|
for (const line of frontmatterBlock.split('\n')) {
|
|
45
77
|
const colonIdx = line.indexOf(':');
|
|
46
78
|
if (colonIdx === -1)
|
|
@@ -60,7 +92,19 @@ export const parseRuleMdcContent = (raw) => {
|
|
|
60
92
|
case 'alwaysApply':
|
|
61
93
|
alwaysApply = value === 'true' || value === '1';
|
|
62
94
|
break;
|
|
95
|
+
case 'kind':
|
|
96
|
+
if (isValidRuleKind(value))
|
|
97
|
+
kind = value;
|
|
98
|
+
break;
|
|
63
99
|
}
|
|
64
100
|
}
|
|
65
|
-
return { description, globs, alwaysApply, body };
|
|
101
|
+
return { description, globs, alwaysApply, kind, body };
|
|
102
|
+
};
|
|
103
|
+
/**
|
|
104
|
+
* Returns kind from the first frontmatter block if present and valid.
|
|
105
|
+
* Use for .md or .mdc files when you only need kind (e.g. solution/skill/command round-trip).
|
|
106
|
+
*/
|
|
107
|
+
export const parseFrontmatterKind = (raw) => {
|
|
108
|
+
const parsed = parseRuleMdcContent(raw);
|
|
109
|
+
return parsed?.kind ?? null;
|
|
66
110
|
};
|
package/dist/lib/rule-cache.js
CHANGED
|
@@ -3,7 +3,7 @@ import { join } from 'path';
|
|
|
3
3
|
import { getConfigDir } from '../auth/config.js';
|
|
4
4
|
import { getRuleById } from '../api/client.js';
|
|
5
5
|
import { ruleFilename, solutionFilename, skillFilename, commandFilename } from './slug.js';
|
|
6
|
-
import { buildRuleMdcContent } from './mdc-format.js';
|
|
6
|
+
import { buildRuleMdcContent, buildMarkdownWithKind } from './mdc-format.js';
|
|
7
7
|
/**
|
|
8
8
|
* Gets the cache directory for rules (~/.bitcompass/cache/rules/)
|
|
9
9
|
*/
|
|
@@ -42,9 +42,7 @@ export const ensureRuleCached = async (id) => {
|
|
|
42
42
|
if (needsUpdate) {
|
|
43
43
|
const content = rule.kind === 'rule'
|
|
44
44
|
? buildRuleMdcContent(rule)
|
|
45
|
-
: rule
|
|
46
|
-
? `# ${rule.title}\n\n${rule.description}\n\n## Solution\n\n${rule.body}\n`
|
|
47
|
-
: `# ${rule.title}\n\n${rule.description}\n\n${rule.body}\n`;
|
|
45
|
+
: buildMarkdownWithKind(rule);
|
|
48
46
|
writeFileSync(cachedPath, content, 'utf-8');
|
|
49
47
|
}
|
|
50
48
|
return cachedPath;
|
|
@@ -5,7 +5,7 @@ import { getRuleById } from '../api/client.js';
|
|
|
5
5
|
import { getProjectConfig } from '../auth/project-config.js';
|
|
6
6
|
import { ruleFilename, solutionFilename, skillFilename, commandFilename } from './slug.js';
|
|
7
7
|
import { ensureRuleCached } from './rule-cache.js';
|
|
8
|
-
import { buildRuleMdcContent } from './mdc-format.js';
|
|
8
|
+
import { buildRuleMdcContent, buildMarkdownWithKind } from './mdc-format.js';
|
|
9
9
|
/**
|
|
10
10
|
* Pulls a rule or solution to a file using symbolic links (like Bun init).
|
|
11
11
|
* Returns the file path where it was written/linked.
|
|
@@ -80,12 +80,10 @@ export const pullRuleToFile = async (id, options = {}) => {
|
|
|
80
80
|
}
|
|
81
81
|
}
|
|
82
82
|
else {
|
|
83
|
-
// Fallback: copy file content (rules as .mdc with frontmatter, others as .md)
|
|
83
|
+
// Fallback: copy file content (rules as .mdc with full frontmatter, others as .md with kind in frontmatter for round-trip)
|
|
84
84
|
const content = rule.kind === 'rule'
|
|
85
85
|
? buildRuleMdcContent(rule)
|
|
86
|
-
: rule
|
|
87
|
-
? `# ${rule.title}\n\n${rule.description}\n\n## Solution\n\n${rule.body}\n`
|
|
88
|
-
: `# ${rule.title}\n\n${rule.description}\n\n${rule.body}\n`;
|
|
86
|
+
: buildMarkdownWithKind(rule);
|
|
89
87
|
writeFileSync(filename, content);
|
|
90
88
|
}
|
|
91
89
|
return filename;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { RuleKind } from '../types.js';
|
|
2
|
+
export declare const SHARE_KIND_CHOICES: Array<{
|
|
3
|
+
value: RuleKind;
|
|
4
|
+
name: string;
|
|
5
|
+
description: string;
|
|
6
|
+
}>;
|
|
7
|
+
export declare const isValidRuleKind: (s: string) => s is RuleKind;
|
|
8
|
+
/**
|
|
9
|
+
* Infers RuleKind from filename prefix (rule-*.mdc, solution-*.md, skill-*.md, command-*.md).
|
|
10
|
+
* Returns null if the basename does not match a known prefix.
|
|
11
|
+
*/
|
|
12
|
+
export declare const inferKindFromFilename: (filePath: string) => RuleKind | null;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
const VALID_KINDS = ['rule', 'solution', 'skill', 'command'];
|
|
2
|
+
export const SHARE_KIND_CHOICES = [
|
|
3
|
+
{
|
|
4
|
+
value: 'rule',
|
|
5
|
+
name: 'Rule',
|
|
6
|
+
description: 'Behaviors, documentation, or how-to for the AI (e.g. i18n guide, coding standards)',
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
value: 'solution',
|
|
10
|
+
name: 'Solution',
|
|
11
|
+
description: 'How we fixed or implemented a specific problem',
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
value: 'skill',
|
|
15
|
+
name: 'Skill',
|
|
16
|
+
description: 'How the AI should behave in a domain (e.g. front-end design, back-end implementation)',
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
value: 'command',
|
|
20
|
+
name: 'Command (workflow)',
|
|
21
|
+
description: 'A workflow or command (e.g. release checklist)',
|
|
22
|
+
},
|
|
23
|
+
];
|
|
24
|
+
export const isValidRuleKind = (s) => VALID_KINDS.includes(s);
|
|
25
|
+
/**
|
|
26
|
+
* Infers RuleKind from filename prefix (rule-*.mdc, solution-*.md, skill-*.md, command-*.md).
|
|
27
|
+
* Returns null if the basename does not match a known prefix.
|
|
28
|
+
*/
|
|
29
|
+
export const inferKindFromFilename = (filePath) => {
|
|
30
|
+
const base = filePath.split(/[/\\]/).pop() ?? '';
|
|
31
|
+
const lower = base.toLowerCase();
|
|
32
|
+
if (lower.startsWith('rule-') && (lower.endsWith('.mdc') || lower.endsWith('.md')))
|
|
33
|
+
return 'rule';
|
|
34
|
+
if (lower.startsWith('solution-') && (lower.endsWith('.md') || lower.endsWith('.mdc')))
|
|
35
|
+
return 'solution';
|
|
36
|
+
if (lower.startsWith('skill-') && (lower.endsWith('.md') || lower.endsWith('.mdc')))
|
|
37
|
+
return 'skill';
|
|
38
|
+
if (lower.startsWith('command-') && (lower.endsWith('.md') || lower.endsWith('.mdc')))
|
|
39
|
+
return 'command';
|
|
40
|
+
return null;
|
|
41
|
+
};
|
package/dist/mcp/server.js
CHANGED
|
@@ -87,7 +87,7 @@ function createStdioServer() {
|
|
|
87
87
|
},
|
|
88
88
|
{
|
|
89
89
|
name: 'post-rules',
|
|
90
|
-
description: 'Use when the user wants to publish or share a new rule, solution, skill, or command to BitCompass. Requires kind, title, and body. Returns the created id and title on success. User must be logged in (bitcompass login).',
|
|
90
|
+
description: 'Use when the user wants to publish or share a new rule, solution, skill, or command to BitCompass. Requires kind, title, and body. When the user says "share" without specifying type, first ask or infer: Rule (behaviors, docs), Solution (how we fixed something), Skill (how AI should behave for X), Command (workflows). Returns the created id and title on success. User must be logged in (bitcompass login).',
|
|
91
91
|
inputSchema: {
|
|
92
92
|
type: 'object',
|
|
93
93
|
properties: {
|
|
@@ -98,6 +98,7 @@ function createStdioServer() {
|
|
|
98
98
|
context: { type: 'string' },
|
|
99
99
|
examples: { type: 'array', items: { type: 'string' } },
|
|
100
100
|
technologies: { type: 'array', items: { type: 'string' } },
|
|
101
|
+
project_id: { type: 'string', description: 'Optional: Compass project UUID to scope this rule to' },
|
|
101
102
|
},
|
|
102
103
|
required: ['kind', 'title', 'body'],
|
|
103
104
|
},
|
|
@@ -121,18 +122,18 @@ function createStdioServer() {
|
|
|
121
122
|
type: 'object',
|
|
122
123
|
properties: {
|
|
123
124
|
id: { type: 'string', description: 'Rule/solution ID' },
|
|
124
|
-
kind: { type: 'string', enum: ['rule', 'solution'], description: 'Optional: filter by kind' },
|
|
125
|
+
kind: { type: 'string', enum: ['rule', 'solution', 'skill', 'command'], description: 'Optional: filter by kind' },
|
|
125
126
|
},
|
|
126
127
|
required: ['id'],
|
|
127
128
|
},
|
|
128
129
|
},
|
|
129
130
|
{
|
|
130
131
|
name: 'list-rules',
|
|
131
|
-
description: 'Use when the user wants to browse or list all rules
|
|
132
|
+
description: 'Use when the user wants to browse or list all rules, solutions, skills, or commands without a search query. Optional kind filter (rule, solution, skill, command) and limit. Returns an array of items with id, title, kind, description, author, snippet, created_at, plus total/returned counts.',
|
|
132
133
|
inputSchema: {
|
|
133
134
|
type: 'object',
|
|
134
135
|
properties: {
|
|
135
|
-
kind: { type: 'string', enum: ['rule', 'solution'], description: 'Optional: filter by kind' },
|
|
136
|
+
kind: { type: 'string', enum: ['rule', 'solution', 'skill', 'command'], description: 'Optional: filter by kind' },
|
|
136
137
|
limit: { type: 'number', description: 'Optional: maximum number of results (default: 50)' },
|
|
137
138
|
},
|
|
138
139
|
},
|
|
@@ -226,6 +227,7 @@ function createStdioServer() {
|
|
|
226
227
|
id,
|
|
227
228
|
result: {
|
|
228
229
|
prompts: [
|
|
230
|
+
{ name: 'share', title: 'Share something', description: 'Guide to publish a rule, solution, skill, or command. Asks what you\'re sharing, then collects content and publishes.' },
|
|
229
231
|
{ name: 'share_new_rule', title: 'Share a new rule', description: 'Guide to collect and publish a reusable rule' },
|
|
230
232
|
{ name: 'share_problem_solution', title: 'Share a problem solution', description: 'Guide to collect and publish a problem solution' },
|
|
231
233
|
],
|
|
@@ -236,6 +238,30 @@ function createStdioServer() {
|
|
|
236
238
|
if (msg.method === 'prompts/get') {
|
|
237
239
|
const params = msg.params;
|
|
238
240
|
const name = params?.name ?? '';
|
|
241
|
+
if (name === 'share') {
|
|
242
|
+
send({
|
|
243
|
+
jsonrpc: '2.0',
|
|
244
|
+
id,
|
|
245
|
+
result: {
|
|
246
|
+
messages: [
|
|
247
|
+
{
|
|
248
|
+
role: 'user',
|
|
249
|
+
content: {
|
|
250
|
+
type: 'text',
|
|
251
|
+
text: `You are helping the user share something to BitCompass. First determine what they are sharing. If they already said (e.g. "share this rule", "share this workflow"), use that; otherwise ask with a single choice:
|
|
252
|
+
- Rule: Behaviors, documentation, or how-to for the AI (e.g. i18n guide, coding standards).
|
|
253
|
+
- Solution: How we fixed or implemented a specific problem.
|
|
254
|
+
- Skill: How the AI should behave in a domain (e.g. front-end design, back-end implementation).
|
|
255
|
+
- Command (workflow): A workflow or command (e.g. release checklist).
|
|
256
|
+
|
|
257
|
+
Then collect: title, description, body (and optionally context, examples, technologies). Ask one question at a time. Then call post-rules with the chosen kind and the collected fields.`,
|
|
258
|
+
},
|
|
259
|
+
},
|
|
260
|
+
],
|
|
261
|
+
},
|
|
262
|
+
});
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
239
265
|
if (name === 'share_new_rule') {
|
|
240
266
|
send({
|
|
241
267
|
jsonrpc: '2.0',
|
|
@@ -352,6 +378,7 @@ function createStdioServer() {
|
|
|
352
378
|
context: args.context || undefined,
|
|
353
379
|
examples: Array.isArray(args.examples) ? args.examples : undefined,
|
|
354
380
|
technologies: Array.isArray(args.technologies) ? args.technologies : undefined,
|
|
381
|
+
project_id: typeof args.project_id === 'string' ? args.project_id : undefined,
|
|
355
382
|
};
|
|
356
383
|
try {
|
|
357
384
|
const created = await insertRule(payload);
|
package/dist/types.d.ts
CHANGED
|
@@ -10,6 +10,8 @@ export interface Rule {
|
|
|
10
10
|
technologies?: string[];
|
|
11
11
|
user_id: string;
|
|
12
12
|
author_display_name?: string | null;
|
|
13
|
+
/** When set, rule is scoped to this Compass project. */
|
|
14
|
+
project_id?: string | null;
|
|
13
15
|
version?: string | null;
|
|
14
16
|
/** Optional glob patterns for when the rule applies (e.g. "*.ts, *.tsx"). Used in .mdc frontmatter. */
|
|
15
17
|
globs?: string | null;
|
|
@@ -26,6 +28,7 @@ export interface RuleInsert {
|
|
|
26
28
|
context?: string | null;
|
|
27
29
|
examples?: string[];
|
|
28
30
|
technologies?: string[];
|
|
31
|
+
project_id?: string | null;
|
|
29
32
|
version?: string;
|
|
30
33
|
globs?: string | null;
|
|
31
34
|
always_apply?: boolean;
|
package/package.json
CHANGED
package/scripts/postinstall.js
CHANGED
|
@@ -99,9 +99,7 @@ const animateAuthorLine = async () => {
|
|
|
99
99
|
process.stdout.write("\r\x1b[K" + finalLine + "\n");
|
|
100
100
|
};
|
|
101
101
|
|
|
102
|
-
(
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
console.log("");
|
|
107
|
-
})();
|
|
102
|
+
console.log("");
|
|
103
|
+
console.log(gradient(`version ${version}`, cyan, magenta));
|
|
104
|
+
await animateAuthorLine();
|
|
105
|
+
console.log("");
|