@bike4mind/cli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,246 @@
1
+ # Bike4Mind CLI
2
+
3
+ Interactive command-line interface for Bike4Mind with ReAct agents.
4
+
5
+ ## Features
6
+
7
+ - 🤖 ReAct agent with reasoning and tool use
8
+ - 💬 Interactive chat interface
9
+ - 💾 Session persistence
10
+ - 🛠️ B4M tools + MCP integration
11
+ - 🎨 Rich terminal UI with Ink
12
+
13
+ ## Installation
14
+
15
+ ### From NPM (Recommended)
16
+
17
+ Install globally:
18
+
19
+ ```bash
20
+ npm install -g @bike4mind/cli
21
+ ```
22
+
23
+ Or run without installing:
24
+
25
+ ```bash
26
+ npx @bike4mind/cli
27
+ ```
28
+
29
+ ### From Source (Development)
30
+
31
+ From the project root:
32
+
33
+ ```bash
34
+ pnpm install
35
+ ```
36
+
37
+ ## Development
38
+
39
+ ```bash
40
+ # Run in development mode
41
+ cd apps/cli
42
+ pnpm dev
43
+
44
+ # Build for production
45
+ pnpm build
46
+
47
+ # Type check
48
+ pnpm typecheck
49
+ ```
50
+
51
+ ## Usage
52
+
53
+ ### Start Interactive Session
54
+
55
+ ```bash
56
+ b4m
57
+ ```
58
+
59
+ ### Quick Query
60
+
61
+ ```bash
62
+ b4m "What's the weather in San Francisco?"
63
+ ```
64
+
65
+ ### With Options
66
+
67
+ ```bash
68
+ # Use specific model
69
+ b4m --model claude-3-5-haiku-20241022
70
+
71
+ # Load saved session
72
+ b4m --session my-project
73
+
74
+ # Debug mode
75
+ b4m --debug
76
+ ```
77
+
78
+ ## Commands
79
+
80
+ While in interactive mode:
81
+
82
+ - `/help` - Show help
83
+ - `/exit` - Exit CLI
84
+ - `/save <name>` - Save current session
85
+ - `/sessions` - List saved sessions
86
+ - `/config` - Show configuration
87
+ - `/models` - List available models
88
+ - `/tools` - List available tools
89
+
90
+ ## Configuration
91
+
92
+ Configuration is stored in `~/.bike4mind/config.json`
93
+
94
+ ### Required: LLM API Keys
95
+
96
+ Set at least one LLM API key in `~/.bike4mind/config.json`:
97
+
98
+ ```json
99
+ {
100
+ "apiKeys": {
101
+ "anthropic": "sk-ant-...",
102
+ "openai": "sk-..."
103
+ },
104
+ "defaultModel": "claude-3-5-sonnet-20241022",
105
+ "preferences": {
106
+ "maxTokens": 4096,
107
+ "temperature": 0.7,
108
+ "autoSave": true
109
+ },
110
+ "mcpServers": []
111
+ }
112
+ ```
113
+
114
+ ### Optional: Tool API Keys
115
+
116
+ Some tools require additional API keys. Add them to your config file:
117
+
118
+ ```json
119
+ {
120
+ "apiKeys": {
121
+ "anthropic": "sk-ant-..."
122
+ },
123
+ "toolApiKeys": {
124
+ "openweather": "your-openweather-key",
125
+ "serper": "your-serper-key"
126
+ },
127
+ "defaultModel": "claude-3-5-sonnet-20241022",
128
+ "preferences": {
129
+ "maxTokens": 4096,
130
+ "temperature": 0.7,
131
+ "autoSave": true
132
+ }
133
+ }
134
+ ```
135
+
136
+ Or use environment variables (config takes precedence):
137
+
138
+ ```bash
139
+ export OPENWEATHER_API_KEY="your-key-here"
140
+ export SERPER_API_KEY="your-key-here"
141
+ ```
142
+
143
+ **Available tools**:
144
+ - ✅ `dice_roll` - No API key needed
145
+ - ✅ `math_evaluate` - No API key needed
146
+ - ✅ `current_datetime` - No API key needed
147
+ - ✅ `prompt_enhancement` - No API key needed
148
+ - 🔑 `weather_info` - Requires `toolApiKeys.openweather`
149
+ - 🔑 `web_search` - Requires `toolApiKeys.serper`
150
+ - 🔑 `deep_research` - Requires `toolApiKeys.serper`
151
+
152
+ **Get API keys**:
153
+ - OpenWeather API: https://openweathermap.org/api
154
+ - Serper API: https://serper.dev/
155
+
156
+ ### Optional: MCP Servers
157
+
158
+ MCP (Model Context Protocol) servers provide additional tools and capabilities. Configure them in your config file:
159
+
160
+ ```json
161
+ {
162
+ "apiKeys": {
163
+ "anthropic": "sk-ant-..."
164
+ },
165
+ "mcpServers": [
166
+ {
167
+ "name": "github",
168
+ "command": "node",
169
+ "args": ["path/to/mcp/server.js"],
170
+ "env": {
171
+ "GITHUB_ACCESS_TOKEN": "ghp_..."
172
+ },
173
+ "enabled": true
174
+ }
175
+ ]
176
+ }
177
+ ```
178
+
179
+ **Available MCP Servers:**
180
+ - 🔧 **GitHub** - Repository management, issues, PRs (requires `GITHUB_ACCESS_TOKEN`)
181
+ - 🔧 **LinkedIn** - Post management, analytics (requires `LINKEDIN_ACCESS_TOKEN`, `COMPANY_NAME`)
182
+ - 🔧 **Atlassian** - Jira/Confluence integration (requires `ATLASSIAN_ACCESS_TOKEN`, `ATLASSIAN_CLOUD_ID`, `ATLASSIAN_SITE_URL`)
183
+
184
+ **Note:** MCP servers must be built and available in the `b4m-core/packages/mcp/dist/src/` directory. The CLI will automatically find them if you're running from the monorepo.
185
+
186
+ ## Session Storage
187
+
188
+ Sessions are saved to `~/.bike4mind/sessions/`
189
+
190
+ Each session includes:
191
+ - Full conversation history
192
+ - Token usage tracking
193
+ - Agent reasoning steps
194
+ - Metadata
195
+
196
+ ## Architecture
197
+
198
+ ```
199
+ apps/cli/
200
+ ├── src/
201
+ │ ├── components/ # Ink React components
202
+ │ │ ├── App.tsx
203
+ │ │ ├── StatusBar.tsx
204
+ │ │ ├── MessageList.tsx
205
+ │ │ ├── InputPrompt.tsx
206
+ │ │ └── ThoughtStream.tsx
207
+ │ ├── storage/ # Local persistence
208
+ │ │ ├── SessionStore.ts
209
+ │ │ └── ConfigStore.ts
210
+ │ └── index.tsx # Main entry point
211
+ └── bin/
212
+ └── bike4mind-cli.js # Executable
213
+ ```
214
+
215
+ ## Dependencies
216
+
217
+ - `@bike4mind/agents` - ReAct agent implementation
218
+ - `@bike4mind/services` - B4M tools
219
+ - `@bike4mind/mcp` - MCP integration
220
+ - `@bike4mind/utils` - LLM backends
221
+ - `ink` - React for CLIs
222
+ - `commander` - CLI argument parsing
223
+ - `lowdb` - Local storage
224
+
225
+ ## Development Status
226
+
227
+ Current implementation includes:
228
+
229
+ ✅ Basic CLI structure
230
+ ✅ ReAct agent integration
231
+ ✅ Session persistence
232
+ ✅ Configuration management
233
+ ✅ Interactive UI with Ink
234
+ ✅ B4M tools integration
235
+ ✅ MCP tools support
236
+
237
+ Coming soon:
238
+
239
+ ⏳ Advanced UI features (streaming visualization, syntax highlighting)
240
+ ⏳ Export functionality (sessions to markdown/JSON)
241
+ ⏳ Session search and management
242
+ ⏳ Tool execution monitoring
243
+
244
+ ## License
245
+
246
+ Private - Bike4Mind
@@ -0,0 +1,66 @@
1
+ #!/usr/bin/env node
2
+ /* eslint-disable */
3
+
4
+ // This is the executable entry point for the CLI
5
+ // In development, it will use tsx to run the TypeScript directly
6
+ // In production, it will run the compiled JavaScript
7
+
8
+ // Suppress punycode deprecation warning from dependencies
9
+ // This is a known issue with older npm packages using Node's built-in punycode
10
+ process.removeAllListeners('warning');
11
+ process.on('warning', (warning) => {
12
+ // Only suppress punycode deprecation warnings
13
+ if (warning.name === 'DeprecationWarning' && warning.message.includes('punycode')) {
14
+ return;
15
+ }
16
+ // Show all other warnings
17
+ console.warn(warning);
18
+ });
19
+
20
+ import { createRequire } from 'module';
21
+ import { fileURLToPath } from 'url';
22
+ import { dirname, join } from 'path';
23
+ import { existsSync } from 'fs';
24
+
25
+ const __filename = fileURLToPath(import.meta.url);
26
+ const __dirname = dirname(__filename);
27
+ const require = createRequire(import.meta.url);
28
+
29
+ // Auto-detect environment: prefer production mode when dist exists
30
+ const distPath = join(__dirname, '../dist/index.js');
31
+ const srcPath = join(__dirname, '../src/index.tsx');
32
+ const hasSource = existsSync(srcPath);
33
+ const hasDist = existsSync(distPath);
34
+
35
+ // Development mode ONLY when:
36
+ // 1. NODE_ENV is explicitly 'development', OR
37
+ // 2. dist doesn't exist but source does (fallback)
38
+ // Otherwise: use production mode (this is what npm users will run)
39
+ const isDev = process.env.NODE_ENV === 'development' ||
40
+ (!hasDist && hasSource);
41
+
42
+ if (isDev) {
43
+ // Show dev mode indicator for developers
44
+ console.log('🔧 Running in development mode (using TypeScript source)\n');
45
+ // Development: use tsx to run TypeScript
46
+ try {
47
+ const { register } = require('tsx/esm/api');
48
+ register();
49
+
50
+ // Import and run the main app
51
+ await import(join(__dirname, '../src/index.tsx'));
52
+ } catch (error) {
53
+ console.error('Failed to start CLI in development mode:', error);
54
+ console.error('\nTry running: pnpm install');
55
+ process.exit(1);
56
+ }
57
+ } else {
58
+ // Production: run compiled JavaScript
59
+ try {
60
+ await import(join(__dirname, '../dist/index.js'));
61
+ } catch (error) {
62
+ console.error('Failed to start CLI:', error);
63
+ console.error('\nTry running: pnpm build');
64
+ process.exit(1);
65
+ }
66
+ }
@@ -0,0 +1,181 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ CurationArtifactType
4
+ } from "./chunk-MKO2KCCS.js";
5
+ import "./chunk-PDX44BCA.js";
6
+
7
+ // ../../b4m-core/packages/services/dist/src/notebookCurationService/artifactExtractor.js
8
+ var ARTIFACT_TAG_REGEX = /<artifact\s+(.*?)>([\s\S]*?)<\/artifact>/gi;
9
+ var ATTRIBUTE_REGEX = /(\w+)=["']([^"']*?)["']/g;
10
+ var CODE_BLOCK_REGEX = /```(\w+)?\s*([\s\S]*?)```/g;
11
+ function extractArtifactsFromMessage(message, options) {
12
+ const artifacts = [];
13
+ const messageId = message.id || message._id?.toString() || "unknown";
14
+ const timestamp = message.timestamp ? new Date(message.timestamp) : /* @__PURE__ */ new Date();
15
+ const textContent = [
16
+ message.prompt || "",
17
+ message.reply || "",
18
+ ...message.replies || [],
19
+ message.questMasterReply || ""
20
+ ].filter(Boolean).join("\n\n");
21
+ if (!textContent) {
22
+ return artifacts;
23
+ }
24
+ if (options.includeCode || options.includeDiagrams || options.includeDataViz) {
25
+ const tagArtifacts = extractArtifactTags(textContent, messageId, timestamp);
26
+ artifacts.push(...tagArtifacts);
27
+ }
28
+ if (options.includeCode) {
29
+ const codeArtifacts = extractCodeBlocks(textContent, messageId, timestamp);
30
+ artifacts.push(...codeArtifacts);
31
+ }
32
+ if (options.includeQuestMaster && message.questMasterPlanId) {
33
+ artifacts.push({
34
+ type: CurationArtifactType.QUESTMASTER_PLAN,
35
+ content: message.questMasterPlanId,
36
+ messageId,
37
+ timestamp,
38
+ metadata: {
39
+ planId: message.questMasterPlanId
40
+ }
41
+ });
42
+ }
43
+ if (options.includeResearch && message.deepResearchState) {
44
+ const research = message.deepResearchState;
45
+ artifacts.push({
46
+ type: CurationArtifactType.DEEP_RESEARCH,
47
+ content: JSON.stringify(research, null, 2),
48
+ messageId,
49
+ timestamp,
50
+ metadata: {
51
+ findingsCount: research.findings?.length || 0,
52
+ sourcesCount: research.sources?.length || 0,
53
+ depth: research.depth,
54
+ completed: research.completed,
55
+ topic: research.topic
56
+ }
57
+ });
58
+ }
59
+ if (options.includeImages && message.images && message.images.length > 0) {
60
+ message.images.forEach((imagePath, index) => {
61
+ artifacts.push({
62
+ type: CurationArtifactType.IMAGE,
63
+ content: imagePath,
64
+ messageId,
65
+ timestamp,
66
+ metadata: {
67
+ index,
68
+ path: imagePath
69
+ }
70
+ });
71
+ });
72
+ }
73
+ return artifacts;
74
+ }
75
+ function extractArtifactTags(content, messageId, timestamp) {
76
+ const artifacts = [];
77
+ let match;
78
+ ARTIFACT_TAG_REGEX.lastIndex = 0;
79
+ while ((match = ARTIFACT_TAG_REGEX.exec(content)) !== null) {
80
+ const [, attributesString, artifactContent] = match;
81
+ const attributes = {};
82
+ let attrMatch;
83
+ ATTRIBUTE_REGEX.lastIndex = 0;
84
+ while ((attrMatch = ATTRIBUTE_REGEX.exec(attributesString)) !== null) {
85
+ const [, key, value] = attrMatch;
86
+ attributes[key] = value;
87
+ }
88
+ const mimeType = attributes.type || "";
89
+ const title = attributes.title || "Untitled";
90
+ const language = attributes.language;
91
+ const identifier = attributes.identifier;
92
+ const artifactType = mapMimeTypeToArtifactType(mimeType);
93
+ if (artifactType) {
94
+ artifacts.push({
95
+ type: artifactType,
96
+ content: artifactContent.trim(),
97
+ language: language || inferLanguageFromType(artifactType),
98
+ messageId,
99
+ timestamp,
100
+ metadata: {
101
+ title,
102
+ identifier,
103
+ mimeType
104
+ }
105
+ });
106
+ }
107
+ }
108
+ return artifacts;
109
+ }
110
+ function extractCodeBlocks(content, messageId, timestamp) {
111
+ const artifacts = [];
112
+ let match;
113
+ CODE_BLOCK_REGEX.lastIndex = 0;
114
+ while ((match = CODE_BLOCK_REGEX.exec(content)) !== null) {
115
+ const [, language, codeContent] = match;
116
+ if (!codeContent || !codeContent.trim()) {
117
+ continue;
118
+ }
119
+ const lineCount = codeContent.trim().split("\n").length;
120
+ if (lineCount < 3) {
121
+ continue;
122
+ }
123
+ const lang = (language || "text").toLowerCase();
124
+ let artifactType;
125
+ if (["mermaid"].includes(lang)) {
126
+ artifactType = CurationArtifactType.MERMAID;
127
+ } else if (["recharts"].includes(lang)) {
128
+ artifactType = CurationArtifactType.RECHARTS;
129
+ } else if (["svg", "xml"].includes(lang)) {
130
+ artifactType = CurationArtifactType.SVG;
131
+ } else if (["html", "htm"].includes(lang)) {
132
+ artifactType = CurationArtifactType.HTML;
133
+ } else if (["tsx", "jsx", "javascript", "typescript", "react"].includes(lang) && (codeContent.includes("useState") || codeContent.includes("useEffect") || codeContent.includes("export default"))) {
134
+ artifactType = CurationArtifactType.REACT;
135
+ } else {
136
+ artifactType = CurationArtifactType.CODE;
137
+ }
138
+ artifacts.push({
139
+ type: artifactType,
140
+ content: codeContent.trim(),
141
+ language: lang || "text",
142
+ messageId,
143
+ timestamp,
144
+ metadata: {
145
+ lineCount
146
+ }
147
+ });
148
+ }
149
+ return artifacts;
150
+ }
151
+ function mapMimeTypeToArtifactType(mimeType) {
152
+ const mimeMap = {
153
+ "application/vnd.ant.react": CurationArtifactType.REACT,
154
+ "text/html": CurationArtifactType.HTML,
155
+ "image/svg+xml": CurationArtifactType.SVG,
156
+ "application/vnd.ant.mermaid": CurationArtifactType.MERMAID,
157
+ "application/vnd.ant.recharts": CurationArtifactType.RECHARTS,
158
+ "application/vnd.ant.code": CurationArtifactType.CODE,
159
+ "text/x.python": CurationArtifactType.CODE,
160
+ "text/x.typescript": CurationArtifactType.CODE,
161
+ "text/x.javascript": CurationArtifactType.CODE
162
+ };
163
+ return mimeMap[mimeType] || null;
164
+ }
165
+ function inferLanguageFromType(type) {
166
+ const languageMap = {
167
+ [CurationArtifactType.CODE]: "typescript",
168
+ [CurationArtifactType.REACT]: "tsx",
169
+ [CurationArtifactType.MERMAID]: "mermaid",
170
+ [CurationArtifactType.RECHARTS]: "json",
171
+ [CurationArtifactType.SVG]: "xml",
172
+ [CurationArtifactType.HTML]: "html",
173
+ [CurationArtifactType.QUESTMASTER_PLAN]: void 0,
174
+ [CurationArtifactType.DEEP_RESEARCH]: "json",
175
+ [CurationArtifactType.IMAGE]: void 0
176
+ };
177
+ return languageMap[type];
178
+ }
179
+ export {
180
+ extractArtifactsFromMessage
181
+ };
@@ -0,0 +1,120 @@
1
+ #!/usr/bin/env node
2
+
3
+ // ../../b4m-core/packages/services/dist/src/mfaService/utils.js
4
+ import speakeasy from "speakeasy";
5
+ import QRCode from "qrcode";
6
+ var originalEmitWarning = process.emitWarning;
7
+ process.emitWarning = (warning, name) => {
8
+ if (name === "DeprecationWarning" && warning?.toString().includes("Buffer")) {
9
+ return;
10
+ }
11
+ return originalEmitWarning.call(process, warning, name);
12
+ };
13
+ process.emitWarning = originalEmitWarning;
14
+ async function generateTOTPSetup(userEmail, appName = "Bike4Mind") {
15
+ const secret = speakeasy.generateSecret({ name: `${appName} (${userEmail})` });
16
+ const qrCodeUrl = await QRCode.toDataURL(secret.otpauth_url);
17
+ return {
18
+ secret: secret.base32,
19
+ qrCodeUrl,
20
+ manualEntryKey: secret.base32
21
+ };
22
+ }
23
+ function verifyTOTPToken(secret, token, window = 1) {
24
+ return speakeasy.totp.verify({
25
+ secret,
26
+ encoding: "base32",
27
+ token,
28
+ window
29
+ // Allow ±30 seconds for clock drift (industry standard)
30
+ });
31
+ }
32
+ function generateBackupCodes(count = 10) {
33
+ return Array.from({ length: count }, () => {
34
+ const secret = speakeasy.generateSecret({ length: 20 });
35
+ return secret.base32.substring(0, 10).toUpperCase();
36
+ });
37
+ }
38
+ function verifyBackupCode(userBackupCodes, providedCode) {
39
+ if (!userBackupCodes || !providedCode) {
40
+ return { isValid: false };
41
+ }
42
+ const cleanProvidedCode = providedCode.trim().toUpperCase();
43
+ const idx = userBackupCodes.findIndex((code) => {
44
+ const cleanStoredCode = code.trim().toUpperCase();
45
+ return cleanStoredCode === cleanProvidedCode;
46
+ });
47
+ if (idx !== -1) {
48
+ return { isValid: true, usedBackupCode: userBackupCodes[idx] };
49
+ }
50
+ return { isValid: false };
51
+ }
52
+ function userRequiresMFA(user, enforceMFASetting) {
53
+ return enforceMFASetting;
54
+ }
55
+ function userHasMFAConfigured(user) {
56
+ return !!(user.mfa && user.mfa.totpEnabled && user.mfa.totpSecret);
57
+ }
58
+ var MAX_FAILED_ATTEMPTS = 3;
59
+ var LOCKOUT_DURATION_MS = 15 * 60 * 1e3;
60
+ var ATTEMPT_RESET_WINDOW_MS = 60 * 60 * 1e3;
61
+ function isUserLockedOut(user) {
62
+ if (!user.mfa?.lockedUntil)
63
+ return false;
64
+ return /* @__PURE__ */ new Date() < new Date(user.mfa.lockedUntil);
65
+ }
66
+ function getLockoutTimeRemaining(user) {
67
+ if (!user.mfa?.lockedUntil)
68
+ return 0;
69
+ const remaining = new Date(user.mfa.lockedUntil).getTime() - Date.now();
70
+ return Math.max(0, Math.ceil(remaining / (60 * 1e3)));
71
+ }
72
+ function shouldResetFailedAttempts(user) {
73
+ if (!user.mfa?.lastFailedAttempt)
74
+ return false;
75
+ const timeSinceLastFailure = Date.now() - new Date(user.mfa.lastFailedAttempt).getTime();
76
+ return timeSinceLastFailure > ATTEMPT_RESET_WINDOW_MS;
77
+ }
78
+ function recordFailedAttempt(user) {
79
+ const currentAttempts = shouldResetFailedAttempts(user) ? 0 : user.mfa?.failedAttempts || 0;
80
+ const newAttempts = currentAttempts + 1;
81
+ const now = /* @__PURE__ */ new Date();
82
+ const updatedMFA = { ...user.mfa };
83
+ updatedMFA.failedAttempts = newAttempts;
84
+ updatedMFA.lastFailedAttempt = now;
85
+ if (newAttempts >= MAX_FAILED_ATTEMPTS) {
86
+ updatedMFA.lockedUntil = new Date(Date.now() + LOCKOUT_DURATION_MS);
87
+ }
88
+ return updatedMFA;
89
+ }
90
+ function clearFailedAttempts(user) {
91
+ if (!user.mfa)
92
+ return null;
93
+ const updatedMFA = { ...user.mfa };
94
+ updatedMFA.failedAttempts = 0;
95
+ updatedMFA.lastFailedAttempt = void 0;
96
+ updatedMFA.lockedUntil = void 0;
97
+ return updatedMFA;
98
+ }
99
+ function userEligibleForMFA(user) {
100
+ return true;
101
+ }
102
+ function userCanDisableMFA(user, enforceMFASetting) {
103
+ return !enforceMFASetting;
104
+ }
105
+
106
+ export {
107
+ generateTOTPSetup,
108
+ verifyTOTPToken,
109
+ generateBackupCodes,
110
+ verifyBackupCode,
111
+ userRequiresMFA,
112
+ userHasMFAConfigured,
113
+ isUserLockedOut,
114
+ getLockoutTimeRemaining,
115
+ shouldResetFailedAttempts,
116
+ recordFailedAttempt,
117
+ clearFailedAttempts,
118
+ userEligibleForMFA,
119
+ userCanDisableMFA
120
+ };