bitcompass 0.2.5 → 0.2.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,16 @@
1
+ import type { EditorProvider, ProjectConfig } from '../types.js';
2
+ declare const EDITOR_DEFAULT_PATHS: Record<EditorProvider, string>;
3
+ declare const DEFAULT_EDITOR: EditorProvider;
4
+ declare const DEFAULT_OUTPUT_PATH: string;
5
+ export declare const getProjectConfigDir: () => string;
6
+ export declare const getEditorDefaultPath: (editor: EditorProvider) => string;
7
+ export declare const loadProjectConfig: () => ProjectConfig | null;
8
+ export declare const saveProjectConfig: (config: ProjectConfig) => void;
9
+ /**
10
+ * Returns project config, or defaults if not configured.
11
+ * When warnIfMissing is true and no config exists, prints a small warning once to stderr and proceeds with defaults.
12
+ */
13
+ export declare const getProjectConfig: (options?: {
14
+ warnIfMissing?: boolean;
15
+ }) => ProjectConfig;
16
+ export { EDITOR_DEFAULT_PATHS, DEFAULT_EDITOR, DEFAULT_OUTPUT_PATH };
@@ -0,0 +1,63 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
2
+ import { join } from 'path';
3
+ const PROJECT_CONFIG_DIR = '.bitcompass';
4
+ const PROJECT_CONFIG_FILE = 'config.json';
5
+ const EDITOR_DEFAULT_PATHS = {
6
+ vscode: '.vscode/rules',
7
+ cursor: '.cursor/rules',
8
+ antigrativity: '.antigrativity/rules',
9
+ claudecode: '.claude/rules',
10
+ };
11
+ const DEFAULT_EDITOR = 'cursor';
12
+ const DEFAULT_OUTPUT_PATH = EDITOR_DEFAULT_PATHS[DEFAULT_EDITOR];
13
+ const getProjectConfigPath = () => {
14
+ const cwd = process.cwd();
15
+ return join(cwd, PROJECT_CONFIG_DIR, PROJECT_CONFIG_FILE);
16
+ };
17
+ export const getProjectConfigDir = () => join(process.cwd(), PROJECT_CONFIG_DIR);
18
+ export const getEditorDefaultPath = (editor) => EDITOR_DEFAULT_PATHS[editor];
19
+ export const loadProjectConfig = () => {
20
+ const path = getProjectConfigPath();
21
+ if (!existsSync(path))
22
+ return null;
23
+ try {
24
+ const raw = readFileSync(path, 'utf-8');
25
+ const data = JSON.parse(raw);
26
+ const editor = data.editor;
27
+ const outputPath = typeof data.outputPath === 'string' ? data.outputPath : undefined;
28
+ if (editor && Object.keys(EDITOR_DEFAULT_PATHS).includes(editor) && outputPath) {
29
+ return { editor, outputPath };
30
+ }
31
+ return null;
32
+ }
33
+ catch {
34
+ return null;
35
+ }
36
+ };
37
+ const ensureProjectConfigDir = () => {
38
+ const dir = join(process.cwd(), PROJECT_CONFIG_DIR);
39
+ if (!existsSync(dir)) {
40
+ mkdirSync(dir, { mode: 0o755, recursive: true });
41
+ }
42
+ };
43
+ export const saveProjectConfig = (config) => {
44
+ ensureProjectConfigDir();
45
+ const path = getProjectConfigPath();
46
+ writeFileSync(path, JSON.stringify(config, null, 2), { mode: 0o644 });
47
+ };
48
+ let warnedMissing = false;
49
+ /**
50
+ * Returns project config, or defaults if not configured.
51
+ * When warnIfMissing is true and no config exists, prints a small warning once to stderr and proceeds with defaults.
52
+ */
53
+ export const getProjectConfig = (options) => {
54
+ const config = loadProjectConfig();
55
+ if (config)
56
+ return config;
57
+ if (options?.warnIfMissing && !warnedMissing) {
58
+ warnedMissing = true;
59
+ process.stderr.write('[bitcompass] No project config found (.bitcompass/config.json). Using defaults. Run "bitcompass init" to configure.\n');
60
+ }
61
+ return { editor: DEFAULT_EDITOR, outputPath: DEFAULT_OUTPUT_PATH };
62
+ };
63
+ export { EDITOR_DEFAULT_PATHS, DEFAULT_EDITOR, DEFAULT_OUTPUT_PATH };
@@ -0,0 +1 @@
1
+ export declare const runInit: () => Promise<void>;
@@ -0,0 +1,56 @@
1
+ import inquirer from 'inquirer';
2
+ import chalk from 'chalk';
3
+ import { existsSync, readFileSync, writeFileSync } from 'fs';
4
+ import { join } from 'path';
5
+ import { getEditorDefaultPath, loadProjectConfig, saveProjectConfig, getProjectConfigDir, } from '../auth/project-config.js';
6
+ const EDITOR_CHOICES = [
7
+ { name: 'VSCode', value: 'vscode' },
8
+ { name: 'Cursor', value: 'cursor' },
9
+ { name: 'Antigrativity', value: 'antigrativity' },
10
+ { name: 'Claude Code', value: 'claudecode' },
11
+ ];
12
+ const GITIGNORE_ENTRY = '.bitcompass';
13
+ const ensureGitignoreEntry = () => {
14
+ const gitignorePath = join(process.cwd(), '.gitignore');
15
+ if (!existsSync(gitignorePath)) {
16
+ writeFileSync(gitignorePath, `${GITIGNORE_ENTRY}\n`, 'utf-8');
17
+ return;
18
+ }
19
+ const content = readFileSync(gitignorePath, 'utf-8');
20
+ const lines = content.split(/\r?\n/);
21
+ const hasEntry = lines.some((line) => line.trim() === GITIGNORE_ENTRY);
22
+ if (hasEntry)
23
+ return;
24
+ const trimmed = content.trimEnd();
25
+ const suffix = trimmed ? '\n' : '';
26
+ writeFileSync(gitignorePath, `${trimmed}${suffix}\n${GITIGNORE_ENTRY}\n`, 'utf-8');
27
+ };
28
+ export const runInit = async () => {
29
+ const existing = loadProjectConfig();
30
+ const answers = await inquirer.prompt([
31
+ {
32
+ name: 'editor',
33
+ message: 'Editor / AI provider',
34
+ type: 'list',
35
+ choices: EDITOR_CHOICES,
36
+ default: existing?.editor ?? 'cursor',
37
+ },
38
+ {
39
+ name: 'outputPath',
40
+ message: 'Folder for rules/docs/commands output',
41
+ type: 'input',
42
+ default: ({ editor }) => getEditorDefaultPath(editor),
43
+ },
44
+ ]);
45
+ const config = {
46
+ editor: answers.editor,
47
+ outputPath: answers.outputPath.trim() || getEditorDefaultPath(answers.editor),
48
+ };
49
+ saveProjectConfig(config);
50
+ ensureGitignoreEntry();
51
+ console.log(chalk.green('Project configured.'));
52
+ console.log(chalk.dim('Config:'), join(getProjectConfigDir(), 'config.json'));
53
+ console.log(chalk.dim('Editor:'), config.editor);
54
+ console.log(chalk.dim('Output path:'), config.outputPath);
55
+ console.log(chalk.dim('.gitignore:'), GITIGNORE_ENTRY, 'added or already present.');
56
+ };
@@ -6,4 +6,19 @@ import type { TimeFrame } from '../lib/git-analysis.js';
6
6
  export declare const buildAndPushActivityLog: (timeFrame: TimeFrame, cwd: string) => Promise<{
7
7
  id: string;
8
8
  }>;
9
- export declare const runLog: () => Promise<void>;
9
+ /**
10
+ * Push an activity log for a custom date or date range. timeFrame is used for display (day/week/month).
11
+ */
12
+ export declare const buildAndPushActivityLogWithPeriod: (period: {
13
+ period_start: string;
14
+ period_end: string;
15
+ since: string;
16
+ }, timeFrame: TimeFrame, cwd: string) => Promise<{
17
+ id: string;
18
+ }>;
19
+ /** Parse argv for log: [start] or [start, end] or [start, '-', end]. Returns { start, end } or null for interactive. */
20
+ export declare const parseLogArgs: (args: string[]) => {
21
+ start: string;
22
+ end?: string;
23
+ } | null;
24
+ export declare const runLog: (args?: string[]) => Promise<void>;
@@ -2,7 +2,7 @@ import inquirer from 'inquirer';
2
2
  import chalk from 'chalk';
3
3
  import { insertActivityLog } from '../api/client.js';
4
4
  import { loadCredentials } from '../auth/config.js';
5
- import { getRepoRoot, getRepoSummary, getGitAnalysis, getPeriodForTimeFrame, } from '../lib/git-analysis.js';
5
+ import { getRepoRoot, getRepoSummary, getGitAnalysis, getPeriodForTimeFrame, getPeriodForCustomDates, } from '../lib/git-analysis.js';
6
6
  /**
7
7
  * Shared logic: resolve repo, compute period, gather summary + git analysis, insert log.
8
8
  * Used by both CLI and MCP. Returns the created log id or throws.
@@ -25,7 +25,55 @@ export const buildAndPushActivityLog = async (timeFrame, cwd) => {
25
25
  const created = await insertActivityLog(payload);
26
26
  return { id: created.id };
27
27
  };
28
- export const runLog = async () => {
28
+ /**
29
+ * Push an activity log for a custom date or date range. timeFrame is used for display (day/week/month).
30
+ */
31
+ export const buildAndPushActivityLogWithPeriod = async (period, timeFrame, cwd) => {
32
+ const repoRoot = getRepoRoot(cwd);
33
+ if (!repoRoot) {
34
+ throw new Error('Not a git repository. Run from a project with git or pass a valid repo path.');
35
+ }
36
+ const repo_summary = getRepoSummary(repoRoot);
37
+ const git_analysis = getGitAnalysis(repoRoot, period.since, period.period_end);
38
+ const payload = {
39
+ time_frame: timeFrame,
40
+ period_start: period.period_start,
41
+ period_end: period.period_end,
42
+ repo_summary: repo_summary,
43
+ git_analysis: git_analysis,
44
+ };
45
+ const created = await insertActivityLog(payload);
46
+ return { id: created.id };
47
+ };
48
+ const ISO_DATE = /^\d{4}-\d{2}-\d{2}$/;
49
+ const isDateArg = (s) => ISO_DATE.test(s.trim());
50
+ /** Parse argv for log: [start] or [start, end] or [start, '-', end]. Returns { start, end } or null for interactive. */
51
+ export const parseLogArgs = (args) => {
52
+ const trimmed = args.map((a) => a.trim()).filter(Boolean);
53
+ if (trimmed.length === 0)
54
+ return null;
55
+ if (trimmed.length === 1 && isDateArg(trimmed[0]))
56
+ return { start: trimmed[0] };
57
+ if (trimmed.length === 2 && isDateArg(trimmed[0]) && isDateArg(trimmed[1])) {
58
+ return { start: trimmed[0], end: trimmed[1] };
59
+ }
60
+ if (trimmed.length === 3 && isDateArg(trimmed[0]) && trimmed[1] === '-' && isDateArg(trimmed[2])) {
61
+ return { start: trimmed[0], end: trimmed[2] };
62
+ }
63
+ throw new Error('Usage: bitcompass log [YYYY-MM-DD] or bitcompass log [YYYY-MM-DD] [YYYY-MM-DD] or bitcompass log [YYYY-MM-DD] - [YYYY-MM-DD]');
64
+ };
65
+ /** Choose time_frame for a custom range by span (day ≤ 1, week ≤ 7, else month). */
66
+ const timeFrameForRange = (start, end) => {
67
+ const a = new Date(start);
68
+ const b = new Date(end);
69
+ const days = Math.round((b.getTime() - a.getTime()) / (24 * 60 * 60 * 1000)) + 1;
70
+ if (days <= 1)
71
+ return 'day';
72
+ if (days <= 7)
73
+ return 'week';
74
+ return 'month';
75
+ };
76
+ export const runLog = async (args = []) => {
29
77
  if (!loadCredentials()) {
30
78
  console.error(chalk.red('Not logged in. Run bitcompass login.'));
31
79
  process.exit(1);
@@ -36,6 +84,14 @@ export const runLog = async () => {
36
84
  console.error(chalk.red('Not a git repository. Run this command from a project with git.'));
37
85
  process.exit(1);
38
86
  }
87
+ const parsed = parseLogArgs(args);
88
+ if (parsed) {
89
+ const period = getPeriodForCustomDates(parsed.start, parsed.end);
90
+ const timeFrame = parsed.end ? timeFrameForRange(parsed.start, parsed.end) : 'day';
91
+ const result = await buildAndPushActivityLogWithPeriod(period, timeFrame, cwd);
92
+ console.log(chalk.green('Log saved.'), chalk.dim(result.id));
93
+ return;
94
+ }
39
95
  const choice = await inquirer.prompt([
40
96
  {
41
97
  name: 'time_frame',
@@ -1,8 +1,10 @@
1
1
  import inquirer from 'inquirer';
2
2
  import ora from 'ora';
3
3
  import chalk from 'chalk';
4
- import { writeFileSync } from 'fs';
4
+ import { mkdirSync, writeFileSync } from 'fs';
5
+ import { join } from 'path';
5
6
  import { loadCredentials } from '../auth/config.js';
7
+ import { getProjectConfig } from '../auth/project-config.js';
6
8
  import { searchRules, fetchRules, getRuleById, insertRule } from '../api/client.js';
7
9
  export const runRulesSearch = async (query) => {
8
10
  if (!loadCredentials()) {
@@ -67,7 +69,10 @@ export const runRulesPull = async (id) => {
67
69
  console.error(chalk.red('Rule not found.'));
68
70
  process.exit(1);
69
71
  }
70
- const filename = `rule-${rule.id}.md`;
72
+ const { outputPath } = getProjectConfig({ warnIfMissing: true });
73
+ const outDir = join(process.cwd(), outputPath);
74
+ mkdirSync(outDir, { recursive: true });
75
+ const filename = join(outDir, `rule-${rule.id}.md`);
71
76
  const content = `# ${rule.title}\n\n${rule.description}\n\n${rule.body}\n`;
72
77
  writeFileSync(filename, content);
73
78
  console.log(chalk.green('Wrote'), filename);
@@ -1,8 +1,10 @@
1
1
  import inquirer from 'inquirer';
2
2
  import ora from 'ora';
3
3
  import chalk from 'chalk';
4
- import { writeFileSync } from 'fs';
4
+ import { mkdirSync, writeFileSync } from 'fs';
5
+ import { join } from 'path';
5
6
  import { loadCredentials } from '../auth/config.js';
7
+ import { getProjectConfig } from '../auth/project-config.js';
6
8
  import { searchRules, fetchRules, getRuleById, insertRule } from '../api/client.js';
7
9
  export const runSolutionsSearch = async (query) => {
8
10
  if (!loadCredentials()) {
@@ -55,7 +57,10 @@ export const runSolutionsPull = async (id) => {
55
57
  console.error(chalk.red('Solution not found.'));
56
58
  process.exit(1);
57
59
  }
58
- const filename = `solution-${rule.id}.md`;
60
+ const { outputPath } = getProjectConfig({ warnIfMissing: true });
61
+ const outDir = join(process.cwd(), outputPath);
62
+ mkdirSync(outDir, { recursive: true });
63
+ const filename = join(outDir, `solution-${rule.id}.md`);
59
64
  const content = `# ${rule.title}\n\n${rule.description}\n\n## Solution\n\n${rule.body}\n`;
60
65
  writeFileSync(filename, content);
61
66
  console.log(chalk.green('Wrote'), filename);
package/dist/index.js CHANGED
@@ -3,6 +3,7 @@ import 'dotenv/config';
3
3
  import chalk from 'chalk';
4
4
  import { Command } from 'commander';
5
5
  import { runConfigGet, runConfigList, runConfigSet } from './commands/config-cmd.js';
6
+ import { runInit } from './commands/init.js';
6
7
  import { runLogin } from './commands/login.js';
7
8
  import { runLogout } from './commands/logout.js';
8
9
  import { runMcpStart, runMcpStatus } from './commands/mcp.js';
@@ -28,9 +29,13 @@ program
28
29
  .description('Show current user (email)')
29
30
  .action(runWhoami);
30
31
  program
31
- .command('log')
32
- .description('Collect repo summary and git activity, then push to your activity logs')
33
- .action(() => runLog().catch(handleErr));
32
+ .command('init')
33
+ .description('Configure project: editor/AI provider and output folder for rules/docs/commands')
34
+ .action(() => runInit().catch(handleErr));
35
+ program
36
+ .command('log [dates...]')
37
+ .description('Collect repo summary and git activity, then push to your activity logs. Optional: bitcompass log YYYY-MM-DD or bitcompass log YYYY-MM-DD YYYY-MM-DD')
38
+ .action((dates) => runLog(dates ?? []).catch(handleErr));
34
39
  const configCmd = program.command('config').description('Show or set config');
35
40
  configCmd.action(runConfigList);
36
41
  configCmd.command('list').description('List config values').action(runConfigList);
@@ -36,7 +36,13 @@ export declare const getRepoSummary: (repoRoot: string) => RepoSummary;
36
36
  * period_end is now; period_start and since are the start of the window.
37
37
  */
38
38
  export declare const getPeriodForTimeFrame: (timeFrame: TimeFrame) => PeriodBounds;
39
+ /**
40
+ * Compute period for custom date(s). Single date = that day; two dates = range (inclusive).
41
+ * period_end is end of last day (23:59:59.999).
42
+ */
43
+ export declare const getPeriodForCustomDates: (startDateStr: string, endDateStr?: string) => PeriodBounds;
39
44
  /**
40
45
  * Run git log for the given period and return structured analysis.
46
+ * If until is provided, commits are limited to the range [since, until].
41
47
  */
42
- export declare const getGitAnalysis: (repoRoot: string, since: string) => GitAnalysisResult;
48
+ export declare const getGitAnalysis: (repoRoot: string, since: string, until?: string) => GitAnalysisResult;
@@ -72,12 +72,61 @@ export const getPeriodForTimeFrame = (timeFrame) => {
72
72
  since,
73
73
  };
74
74
  };
75
+ /**
76
+ * Parse an ISO date string (YYYY-MM-DD) to Date at start of day (UTC).
77
+ */
78
+ const parseDate = (s) => {
79
+ const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(s.trim());
80
+ if (!match)
81
+ return null;
82
+ const y = parseInt(match[1], 10);
83
+ const m = parseInt(match[2], 10) - 1;
84
+ const d = parseInt(match[3], 10);
85
+ const date = new Date(Date.UTC(y, m, d, 0, 0, 0, 0));
86
+ if (date.getUTCFullYear() !== y || date.getUTCMonth() !== m || date.getUTCDate() !== d)
87
+ return null;
88
+ return date;
89
+ };
90
+ /**
91
+ * Compute period for custom date(s). Single date = that day; two dates = range (inclusive).
92
+ * period_end is end of last day (23:59:59.999).
93
+ */
94
+ export const getPeriodForCustomDates = (startDateStr, endDateStr) => {
95
+ const startDate = parseDate(startDateStr);
96
+ if (!startDate) {
97
+ throw new Error(`Invalid start date: ${startDateStr}. Use YYYY-MM-DD.`);
98
+ }
99
+ let periodEnd;
100
+ if (endDateStr !== undefined && endDateStr.trim() !== '') {
101
+ const endDate = parseDate(endDateStr);
102
+ if (!endDate)
103
+ throw new Error(`Invalid end date: ${endDateStr}. Use YYYY-MM-DD.`);
104
+ if (endDate < startDate)
105
+ throw new Error('End date must be on or after start date.');
106
+ periodEnd = new Date(endDate);
107
+ periodEnd.setUTCHours(23, 59, 59, 999);
108
+ }
109
+ else {
110
+ periodEnd = new Date(startDate);
111
+ periodEnd.setUTCHours(23, 59, 59, 999);
112
+ }
113
+ const period_start = new Date(startDate);
114
+ period_start.setUTCHours(0, 0, 0, 0);
115
+ const since = period_start.toISOString();
116
+ return {
117
+ period_start: period_start.toISOString(),
118
+ period_end: periodEnd.toISOString(),
119
+ since,
120
+ };
121
+ };
75
122
  /**
76
123
  * Run git log for the given period and return structured analysis.
124
+ * If until is provided, commits are limited to the range [since, until].
77
125
  */
78
- export const getGitAnalysis = (repoRoot, since) => {
126
+ export const getGitAnalysis = (repoRoot, since, until) => {
79
127
  const format = '%H%x00%s%x00%ci';
80
- const logOut = exec(`git log --since="${since}" --format=${format}`, repoRoot);
128
+ const untilArg = until ? ` --until="${until}"` : '';
129
+ const logOut = exec(`git log --since="${since}"${untilArg} --format=${format}`, repoRoot);
81
130
  const commits = [];
82
131
  if (logOut) {
83
132
  for (const line of logOut.split('\n')) {
@@ -91,7 +140,7 @@ export const getGitAnalysis = (repoRoot, since) => {
91
140
  }
92
141
  }
93
142
  }
94
- const shortstatOut = exec(`git log --since="${since}" --shortstat --format=`, repoRoot);
143
+ const shortstatOut = exec(`git log --since="${since}"${untilArg} --shortstat --format=`, repoRoot);
95
144
  let insertions = 0;
96
145
  let deletions = 0;
97
146
  if (shortstatOut) {
@@ -1,6 +1,7 @@
1
1
  import { AUTH_REQUIRED_MSG, insertRule, searchRules } from '../api/client.js';
2
2
  import { buildAndPushActivityLog } from '../commands/log.js';
3
3
  import { loadCredentials } from '../auth/config.js';
4
+ import { getProjectConfig } from '../auth/project-config.js';
4
5
  /** When token is missing, we fail initialize so Cursor shows "Needs authentication" (yellow) instead of success (green). */
5
6
  const NEEDS_AUTH_ERROR_MESSAGE = 'Needs authentication';
6
7
  const NEEDS_AUTH_ERROR_CODE = -32001; // Server error: auth required
@@ -276,6 +277,7 @@ function createStdioServer() {
276
277
  };
277
278
  }
278
279
  export const startMcpServer = async () => {
280
+ getProjectConfig({ warnIfMissing: true });
279
281
  const server = createStdioServer();
280
282
  await server.connect();
281
283
  // Do not exit when not logged in: Cursor needs the process alive to complete
package/dist/types.d.ts CHANGED
@@ -30,6 +30,12 @@ export interface StoredCredentials {
30
30
  email?: string;
31
31
  };
32
32
  }
33
+ export type EditorProvider = 'vscode' | 'cursor' | 'antigrativity' | 'claudecode';
34
+ export interface ProjectConfig {
35
+ editor: EditorProvider;
36
+ /** Folder for rules/docs/commands output (e.g. .cursor/rules/) */
37
+ outputPath: string;
38
+ }
33
39
  export interface BitcompassConfig {
34
40
  apiUrl?: string;
35
41
  supabaseUrl?: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bitcompass",
3
- "version": "0.2.5",
3
+ "version": "0.2.7",
4
4
  "description": "BitCompass CLI - rules, solutions, and MCP server",
5
5
  "type": "module",
6
6
  "bin": {