Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions docs/cli/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,31 @@ Slash commands provide meta-level control over the CLI itself.
- **User-level:** `~/.blackboxcli/agents/` (personal agents, available across projects)
- **Note:** For detailed information on creating and managing subagents, see the [Subagents documentation](../subagents.md).

- **`/datasource`**
- **Description:** Manage data source configurations for MongoDB and Supabase databases. Configure named data sources to use with the `/data` command.
- **Sub-commands:**
- **`create`**:
- **Description:** Launch an interactive wizard to create a new data source configuration. The wizard guides you through selecting the database type (MongoDB or Supabase), naming the data source, and entering connection credentials.
- **Usage:** `/datasource create`
- **`manage`**:
- **Description:** Open an interactive management dialog to view and delete existing data source configurations.
- **Usage:** `/datasource manage`
- **Storage Location:**
- **Project-level:** `.blackboxcli/data-configs/` (shared with team)
- **Note:** Data source configurations are stored as JSON files and contain connection credentials. These are stored in plain text, so be mindful of security when sharing project directories.

- **`/data`**
- **Description:** Query a configured data source using natural language. The AI will automatically use the appropriate database tool (mongo_find or supabase_select) with the configured credentials.
- **Usage:** `/data <data-source-name> "query text"`
- **Examples:**
- `/data movies "find me all movies released in 2025"`
- `/data user-profiles "show me users with status active"`
- **Details:**
- The command requires a data source name and a query string in quotes
- The data source must be configured using `/datasource create` first
- The AI will automatically convert your natural language query into appropriate database query parameters
- Supports tab completion for data source names

- [**`/tools`**](../tools/index.md)
- **Description:** Display a list of tools that are currently available within Blackbox Code.
- **Usage:** `/tools [desc]`
Expand Down
4 changes: 4 additions & 0 deletions packages/cli/src/services/BuiltinCommandLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ import { themeCommand } from '../ui/commands/themeCommand.js';
import { toolsCommand } from '../ui/commands/toolsCommand.js';
import { vimCommand } from '../ui/commands/vimCommand.js';
import { setupGithubCommand } from '../ui/commands/setupGithubCommand.js';
import { datasourceCommand } from '../ui/commands/datasourceCommand.js';
import { dataCommand } from '../ui/commands/dataCommand.js';

/**
* Loads the core, hard-coded slash commands that are an integral part
Expand Down Expand Up @@ -87,6 +89,8 @@ export class BuiltinCommandLoader implements ICommandLoader {
vimCommand,
setupGithubCommand,
terminalSetupCommand,
datasourceCommand,
dataCommand,
];

return allDefinitions.filter((cmd): cmd is SlashCommand => cmd !== null);
Expand Down
34 changes: 34 additions & 0 deletions packages/cli/src/ui/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ import { useDialogClose } from './hooks/useDialogClose.js';
import { useSlashCommandProcessor } from './hooks/slashCommandProcessor.js';
import { useSubagentCreateDialog } from './hooks/useSubagentCreateDialog.js';
import { useAgentsManagerDialog } from './hooks/useAgentsManagerDialog.js';
import { useDatasourceCreateDialog } from './hooks/useDatasourceCreateDialog.js';
import { useDatasourceManagerDialog } from './hooks/useDatasourceManagerDialog.js';
import { useAutoAcceptIndicator } from './hooks/useAutoAcceptIndicator.js';
import { useMessageQueue } from './hooks/useMessageQueue.js';
import { useConsoleMessages } from './hooks/useConsoleMessages.js';
Expand Down Expand Up @@ -69,6 +71,10 @@ import {
AgentCreationWizard,
AgentsManagerDialog,
} from './components/subagents/index.js';
import {
DatasourceCreationWizard,
DatasourceManagerDialog,
} from './components/datasources/index.js';
import { Colors } from './colors.js';
import { loadHierarchicalGeminiMemory } from '../config/config.js';
import type { LoadedSettings } from '../config/settings.js';
Expand Down Expand Up @@ -338,6 +344,18 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
closeAgentsManagerDialog,
} = useAgentsManagerDialog();

const {
isDatasourceCreateDialogOpen,
openDatasourceCreateDialog,
closeDatasourceCreateDialog,
} = useDatasourceCreateDialog();

const {
isDatasourceManagerDialogOpen,
openDatasourceManagerDialog,
closeDatasourceManagerDialog,
} = useDatasourceManagerDialog();

const { isFolderTrustDialogOpen, handleFolderTrustSelect, isRestarting } =
useFolderTrust(settings, setIsTrustedFolder);

Expand Down Expand Up @@ -785,6 +803,8 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
handleModelSelectionOpen,
openSubagentCreateDialog,
openAgentsManagerDialog,
openDatasourceCreateDialog,
openDatasourceManagerDialog,
openHistoryBrowser,
toggleVimEnabled,
setIsProcessing,
Expand Down Expand Up @@ -1425,6 +1445,20 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
config={config}
/>
</Box>
) : isDatasourceCreateDialogOpen ? (
<Box flexDirection="column">
<DatasourceCreationWizard
onClose={closeDatasourceCreateDialog}
config={config}
/>
</Box>
) : isDatasourceManagerDialogOpen ? (
<Box flexDirection="column">
<DatasourceManagerDialog
onClose={closeDatasourceManagerDialog}
config={config}
/>
</Box>
) : isHistoryBrowserOpen && logger ? (
<Box flexDirection="column">
<HistoryBrowserDialog
Expand Down
240 changes: 240 additions & 0 deletions packages/cli/src/ui/commands/dataCommand.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/

import {
CommandKind,
type SlashCommand,
type CommandContext,
type SlashCommandActionReturn,
type SubmitPromptActionReturn,
type MessageActionReturn,
} from './types.js';
import type { DataConfig } from '@blackbox_ai/blackbox-cli-core';

/**
* Parses the /data command arguments.
* Expected format: /data <data-name> "query text"
*/
function parseDataCommand(args: string): {
dataSourceName: string;
query: string;
} | null {
const trimmed = args.trim();
if (!trimmed) {
return null;
}

// Try to find a quoted string (the query)
// Match: name "query" or name 'query'
const quotedMatch = trimmed.match(/^([^\s"']+)\s+(["'])(.*)\2$/s);
if (quotedMatch) {
return {
dataSourceName: quotedMatch[1],
query: quotedMatch[3],
};
}

// If no quotes, try splitting on first space and treat rest as query
const spaceIndex = trimmed.indexOf(' ');
if (spaceIndex > 0) {
return {
dataSourceName: trimmed.substring(0, spaceIndex),
query: trimmed.substring(spaceIndex + 1),
};
}

// Just the data source name, no query
return {
dataSourceName: trimmed,
query: '',
};
}

/**
* Parses a MongoDB URI to extract the database name.
* Supports formats like:
* - mongodb://localhost:27017/dbname
* - mongodb://user:pass@host:port/dbname
* - mongodb+srv://user:pass@cluster.mongodb.net/dbname?options
*/
function parseMongoDBDatabaseName(uri: string): string | null {
try {
// For mongodb+srv, we need to handle it differently since URL doesn't parse it well
if (uri.startsWith('mongodb+srv://')) {
// Extract path after the domain, e.g., mongodb+srv://cluster.net/dbname?options
const match = uri.match(/mongodb\+srv:\/\/[^/]+\/([^/?]+)/);
if (match && match[1]) {
return match[1];
}
return null;
}

// For regular mongodb:// URIs, try parsing as URL
// Replace mongodb:// with http:// for URL parsing
const httpUri = uri.replace(/^mongodb:/, 'http:');
const url = new URL(httpUri);
// Get the pathname (e.g., "/dbname" or "/dbname?options")
const pathname = url.pathname;
// Remove leading slash and any query parameters or additional path segments
const dbName = pathname.replace(/^\/+/, '').split('?')[0].split('/')[0].trim();
return dbName || null;
} catch {
// Fallback: try to extract database name manually using regex
// Match pattern: /dbname or /dbname?options (but not after // which is the protocol)
// Look for /dbname after the host part
const match = uri.match(/(?:\/\/[^/]+\/)([^/?]+)(?:\?|$)/);
if (match && match[1]) {
return match[1];
}
return null;
}
}

/**
* Creates a context-aware prompt that instructs the AI to use the specified data source.
*/
function createDataQueryPrompt(
dataConfig: DataConfig,
userQuery: string,
): string {
let dbNameHint = '';
let dbName = null;

if (dataConfig.type === 'mongodb') {
const uri = (dataConfig.credentials as { uri: string }).uri;
dbName = parseMongoDBDatabaseName(uri);
if (dbName) {
dbNameHint = `\n\nCRITICAL: The database name extracted from the MongoDB URI is "${dbName}". You MUST use "${dbName}" as the \`db\` parameter value. Do NOT use "${dataConfig.name}" as the database name - "${dataConfig.name}" is only the data source configuration name for identifying which credentials to use.`;
}
}

const dataSourceInfo =
dataConfig.type === 'mongodb'
? `MongoDB database. Use the mongo_find tool with the dataSource parameter set to "${dataConfig.name}".`
: `Supabase database. Use the supabase_select tool with the dataSource parameter set to "${dataConfig.name}".`;

const toolName =
dataConfig.type === 'mongodb' ? 'mongo_find' : 'supabase_select';

if (!userQuery) {
return `You have access to a configured data source named "${dataConfig.name}" which is a ${dataSourceInfo}${dbNameHint}

Please help the user query this data source. Ask them what they would like to know.`;
}

const dbParamHint = dbName
? `\n\nWhen calling ${toolName}, use these parameters:
- \`dataSource\`: "${dataConfig.name}" (the configuration name)
- \`db\`: "${dbName}" (the actual database name from the URI, NOT "${dataConfig.name}")`
: `\n\nWhen calling ${toolName}, use:
- \`dataSource\`: "${dataConfig.name}"`;

return `The user wants to query their configured data source "${dataConfig.name}" which is a ${dataSourceInfo}${dbNameHint}

User query: "${userQuery}"

Please use the ${toolName} tool to execute this query. Convert the natural language query into appropriate tool parameters.${dbParamHint}

Important: Always include the dataSource parameter with value "${dataConfig.name}" when calling the tool.`;
}

export const dataCommand: SlashCommand = {
name: 'data',
description:
'Query a configured data source. Usage: /data <data-source-name> "query text"',
kind: CommandKind.BUILT_IN,
action: async (
context: CommandContext,
args: string,
): Promise<SlashCommandActionReturn> => {
const { services } = context;
const { config } = services;

if (!config) {
return {
type: 'message',
messageType: 'error',
content: 'Configuration not available.',
} as MessageActionReturn;
}

// Parse command arguments
const parsed = parseDataCommand(args);
if (!parsed) {
return {
type: 'message',
messageType: 'error',
content:
'Invalid command format. Usage: /data <data-source-name> "query text"',
} as MessageActionReturn;
}

const { dataSourceName, query } = parsed;

if (!dataSourceName) {
return {
type: 'message',
messageType: 'error',
content: 'Data source name is required.',
} as MessageActionReturn;
}

// Load the data configuration
try {
const dataConfigManager = config.getDataConfigManager();
const dataConfig = await dataConfigManager.loadDataConfig(dataSourceName);

if (!dataConfig) {
return {
type: 'message',
messageType: 'error',
content: `Data source "${dataSourceName}" not found. Please configure it using /datasource create.`,
} as MessageActionReturn;
}

// Create the context-aware prompt
const prompt = createDataQueryPrompt(dataConfig, query);

// Submit the prompt to the AI
return {
type: 'submit_prompt',
content: prompt,
} as SubmitPromptActionReturn;
} catch (error) {
return {
type: 'message',
messageType: 'error',
content: `Error loading data source configuration: ${error instanceof Error ? error.message : 'Unknown error'}`,
} as MessageActionReturn;
}
},
completion: async (
context: CommandContext,
partialArg: string,
): Promise<string[]> => {
const { services } = context;
const { config } = services;

if (!config) {
return [];
}

try {
const dataConfigManager = config.getDataConfigManager();
const dataConfigs = await dataConfigManager.listDataConfigs();

// Filter by partial match and return just the names
const matches = dataConfigs
.filter((dc) => dc.name.startsWith(partialArg))
.map((dc) => dc.name);

return matches;
} catch {
return [];
}
},
};

38 changes: 38 additions & 0 deletions packages/cli/src/ui/commands/datasourceCommand.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/

import {
CommandKind,
type SlashCommand,
type OpenDialogActionReturn,
} from './types.js';

export const datasourceCommand: SlashCommand = {
name: 'datasource',
description: 'Manage data source configurations for MongoDB and Supabase.',
kind: CommandKind.BUILT_IN,
subCommands: [
{
name: 'create',
description: 'Create a new data source configuration.',
kind: CommandKind.BUILT_IN,
action: (): OpenDialogActionReturn => ({
type: 'dialog',
dialog: 'datasource_create',
}),
},
{
name: 'manage',
description: 'Manage existing data source configurations (view, edit, delete).',
kind: CommandKind.BUILT_IN,
action: (): OpenDialogActionReturn => ({
type: 'dialog',
dialog: 'datasource_list',
}),
},
],
};

Loading