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
75 changes: 73 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,7 @@ This is particularly useful for questions that span multiple files or concepts,
| `mgrep` / `mgrep search <pattern> [path]` | Natural-language search with many `grep`-style flags (`-i`, `-r`, `-m`...). |
| `mgrep watch` | Index current repo and keep the Mixedbread store in sync via file watchers. |
| `mgrep login` & `mgrep logout` | Manage device-based authentication with Mixedbread. |
| `mgrep switch-org` | Switch to a different organization. |
| `mgrep install-claude-code` | Authenticate, add the Mixedbread mgrep plugin to Claude Code. |
| `mgrep install-opencode` | Authenticate and add the Mixedbread mgrep to OpenCode. |
| `mgrep install-codex` | Authenticate and add the Mixedbread mgrep to Codex. |
Expand All @@ -207,6 +208,7 @@ directory for a pattern.
| `--agentic` | Enable agentic search to automatically refine queries and perform multiple searches |
| `-s`, `--sync` | Sync the local files to the store before searching |
| `-d`, `--dry-run` | Dry run the search process (no actual file syncing) |
| `-S`, `--shared` | Enable shared mode for multi-user collaboration |
| `--no-rerank` | Disable reranking of search results |
| `--max-file-size <bytes>` | Maximum file size in bytes to upload (overrides config) |
| `--max-file-count <count>` | Maximum number of files to upload (overrides config) |
Expand Down Expand Up @@ -235,12 +237,14 @@ root of the repository. The `.mgrepignore` file follows the same syntax as the
| Option | Description |
| --- | --- |
| `-d`, `--dry-run` | Dry run the watch process (no actual file syncing) |
| `-S`, `--shared` | Enable shared mode for multi-user collaboration |
| `--max-file-size <bytes>` | Maximum file size in bytes to upload (overrides config) |
| `--max-file-count <count>` | Maximum number of files to upload (overrides config) |

**Examples:**
```bash
mgrep watch # index the current repository and keep the Mixedbread store in sync via file watchers
mgrep watch --shared # index with shared mode for multi-user collaboration
mgrep watch --max-file-size 1048576 # limit uploads to files under 1MB
mgrep watch --max-file-count 5000 # limit sync to 5000 changed files or fewer
```
Expand All @@ -254,6 +258,69 @@ mgrep watch --max-file-count 5000 # limit sync to 5000 changed files or fewer
- Results include relative paths plus contextual hints (line ranges for text, page numbers for PDFs, etc.) for a skim-friendly experience.
- Because stores are cloud-backed, agents and teammates can query the same corpus without re-uploading.

## Multi-User / Shared Mode

When multiple users in an organization want to share the same store for a project, use **shared mode**. Each user uploads files with their own absolute paths. Search uses regex suffix matching to find results across all users, regardless of their local directory structure.

### Enabling Shared Mode

You can enable shared mode in three ways:

1. **CLI flag**: Add `-S` or `--shared` to your commands
```bash
mgrep watch --shared
mgrep --shared "where is auth configured?"
```

2. **Environment variable**: Set `MGREP_SHARED=true`
```bash
export MGREP_SHARED=true
mgrep watch
```

3. **Config file**: Add `shared: true` to your `.mgreprc.yaml`
```yaml
shared: true
maxFileSize: 10485760
```

### How It Works

Without shared mode, files are stored with absolute paths and search uses `starts_with` to scope results to the current user's directory. This works fine for single users.

With shared mode enabled:
- Files are still stored with **absolute paths** (each user keeps their own paths)
- Search uses **regex suffix matching** to find results from all users in the store
- Any team member can sync and search the store regardless of their local path

### Multi-User Workflow

1. **First user indexes the project:**
```bash
cd /Users/alice/projects/myapp
mgrep watch --shared --store myapp-team
```

2. **Other team members join:**
```bash
cd /home/bob/code/myapp
mgrep --shared --store myapp-team "how does authentication work?"
```

3. **All users search the same store:**
```bash
mgrep --shared --store myapp-team "database connection pooling"
```

### Organization Support

mgrep supports Mixedbread organizations for team collaboration:

- **Login with organization**: When you log in, you'll be prompted to select an organization if you belong to multiple
- **Switch organizations**: Use `mgrep switch-org` to change your active organization

Stores are scoped to organizations, so different teams can have stores with the same name without conflicts.

## Configuration

mgrep can be configured via config files, environment variables, or CLI flags.
Expand All @@ -268,11 +335,14 @@ maxFileSize: 5242880

# Maximum number of files to sync (upload/delete) per operation (default: 1000)
maxFileCount: 5000

# Enable shared mode for multi-user collaboration (default: false)
shared: true
```

**Configuration precedence** (highest to lowest):
1. CLI flags (`--max-file-size`, `--max-file-count`)
2. Environment variables (`MGREP_MAX_FILE_SIZE`, `MGREP_MAX_FILE_COUNT`)
1. CLI flags (`--max-file-size`, `--max-file-count`, `--shared`)
2. Environment variables (`MGREP_MAX_FILE_SIZE`, `MGREP_MAX_FILE_COUNT`, `MGREP_SHARED`)
3. Local config file (`.mgreprc.yaml` in project directory)
4. Global config file (`~/.config/mgrep/config.yaml`)
5. Default values
Expand Down Expand Up @@ -310,6 +380,7 @@ searches.

- `MGREP_MAX_FILE_SIZE`: Maximum file size in bytes to upload (default: `1048576` / 1MB)
- `MGREP_MAX_FILE_COUNT`: Maximum number of files to sync per operation (default: `1000`)
- `MGREP_SHARED`: Enable shared mode for multi-user collaboration (set to `1` or `true` to enable)

**Examples:**
```bash
Expand Down
162 changes: 162 additions & 0 deletions src/commands/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
import * as fs from "node:fs";
import type { Command } from "commander";
import { Command as CommanderCommand } from "commander";
import {
CONFIG_KEYS,
type ConfigKey,
DEFAULT_CONFIG,
getGlobalConfigFilePath,
readGlobalConfig,
saveGlobalConfig,
writeGlobalConfig,
} from "../lib/config.js";

/**
* Parses a string value into the correct type for a given config key.
*
* @param key - The config key
* @param value - The raw string value
* @returns The parsed value
*/
function parseConfigValue(
key: ConfigKey,
value: string,
): number | boolean {
switch (key) {
case "maxFileSize":
case "maxFileCount": {
const parsed = Number.parseInt(value, 10);
if (Number.isNaN(parsed) || parsed <= 0) {
throw new Error(`Value for "${key}" must be a positive integer.`);
}
return parsed;
}
case "shared": {
const lower = value.toLowerCase();
if (
lower === "true" ||
lower === "1" ||
lower === "yes" ||
lower === "y"
) {
return true;
}
if (
lower === "false" ||
lower === "0" ||
lower === "no" ||
lower === "n"
) {
return false;
}
throw new Error(
`Value for "${key}" must be a boolean (true/false, 1/0, yes/no).`,
);
}
}
}

/**
* Validates that a string is a valid config key.
*
* @param key - The string to validate
* @returns The validated config key
*/
function validateKey(key: string): ConfigKey {
if (!CONFIG_KEYS.includes(key as ConfigKey)) {
throw new Error(
`Unknown config key "${key}". Valid keys: ${CONFIG_KEYS.join(", ")}`,
);
}
return key as ConfigKey;
}

const set = new CommanderCommand("set")
.description("Set a global config value")
.argument("<key>", `Config key (${CONFIG_KEYS.join(", ")})`)
.argument("<value>", "Config value")
.action((rawKey: string, rawValue: string) => {
try {
const key = validateKey(rawKey);
const value = parseConfigValue(key, rawValue);
writeGlobalConfig({ [key]: value });
console.log(`Set ${key} = ${value}`);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
console.error(`Error: ${message}`);
process.exitCode = 1;
}
});

const get = new CommanderCommand("get")
.description("Get a global config value")
.argument("<key>", `Config key (${CONFIG_KEYS.join(", ")})`)
.action((rawKey: string) => {
try {
const key = validateKey(rawKey);
const globalConfig = readGlobalConfig();
const value = globalConfig[key];
if (value === undefined) {
console.log(`${key} = ${DEFAULT_CONFIG[key]} (default)`);
} else {
console.log(`${key} = ${value}`);
}
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
console.error(`Error: ${message}`);
process.exitCode = 1;
}
});

const list = new CommanderCommand("list")
.description("List all global config values")
.action(() => {
const globalConfig = readGlobalConfig();
for (const key of CONFIG_KEYS) {
const value = globalConfig[key];
if (value === undefined) {
console.log(`${key} = ${DEFAULT_CONFIG[key]} (default)`);
} else {
console.log(`${key} = ${value}`);
}
}
});

const reset = new CommanderCommand("reset")
.description("Reset global config to defaults")
.argument("[key]", "Config key to reset (omit to reset all)")
.action((rawKey?: string) => {
try {
if (rawKey) {
const key = validateKey(rawKey);
const globalConfig = readGlobalConfig();
delete globalConfig[key];
if (Object.keys(globalConfig).length === 0) {
const filePath = getGlobalConfigFilePath();
if (fs.existsSync(filePath)) {
fs.unlinkSync(filePath);
}
} else {
saveGlobalConfig(globalConfig);
}
console.log(`Reset ${key} to default (${DEFAULT_CONFIG[key]})`);
} else {
const filePath = getGlobalConfigFilePath();
if (fs.existsSync(filePath)) {
fs.unlinkSync(filePath);
}
console.log("Reset all config to defaults");
}
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
console.error(`Error: ${message}`);
process.exitCode = 1;
}
});

export const config: Command = new CommanderCommand("config")
.description("Manage global mgrep configuration")
.addCommand(set)
.addCommand(get)
.addCommand(list)
.addCommand(reset);
2 changes: 2 additions & 0 deletions src/commands/logout.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { outro } from "@clack/prompts";
import chalk from "chalk";
import { Command } from "commander";
import { clearCachedOrganization } from "../lib/organizations.js";
import { deleteToken, getStoredToken } from "../lib/token.js";

export async function logoutAction() {
Expand All @@ -11,6 +12,7 @@ export async function logoutAction() {
}

await deleteToken();
await clearCachedOrganization();
outro(chalk.green("✅ Successfully logged out"));
}

Expand Down
Loading
Loading