Skip to content

Commit 17ac3ba

Browse files
authored
feat(cli): add non-interactive workspace create and mcp-serve --workspace flag (#100)
* feat(cli): add non-interactive workspace create and mcp-serve --workspace flag Add CLI flags (--name, --backend, --embedder-provider, --embedder-model, --dsn) to workspace create for scripted/CI usage. Add --workspace flag to mcp-serve so MCP tools can be scoped to a specific workspace. Add FindWorkspaceConfig() and WorkspaceStoreConfig() helpers in config package. * docs: add workspace mode, parallelism tiers, and MCP workspace sections Update workspace, MCP, embedders, and watch-guide documentation with workspace mode usage, parallelism tier reference, and MCP workspace-scoped configuration examples. Add CHANGELOG entry for unreleased changes.
1 parent 234c109 commit 17ac3ba

File tree

14 files changed

+1095
-76
lines changed

14 files changed

+1095
-76
lines changed

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,3 +42,7 @@ docs/node_modules/
4242
docs/.astro/
4343
docs/bun.lock
4444
__debug*
45+
46+
# Local tooling
47+
.pre-commit-config.yaml
48+
docs/plans/

CHANGELOG.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Added
11+
12+
- **Non-Interactive Workspace Create**: `workspace create` now supports `--name`, `--backend`, `--embedder-provider`, `--embedder-model`, `--dsn` flags for scripted/CI usage
13+
- Enables fully non-interactive workspace creation without TUI prompts
14+
- All required parameters can be passed as CLI flags
15+
- **MCP Serve Workspace Flag**: `mcp-serve --workspace <name>` to scope MCP tools to a specific workspace
16+
- MCP search and trace tools automatically use the workspace context
17+
- **Workspace Config Helpers**: `FindWorkspaceConfig()` and `WorkspaceStoreConfig()` in config package for programmatic workspace resolution
18+
19+
### Documentation
20+
21+
- Updated workspace docs with workspace mode, parallelism tiers, and MCP workspace sections
22+
- Updated MCP docs with workspace-scoped configuration examples
23+
- Updated embedders docs with parallelism tier reference
24+
- Updated watch guide with workspace daemon examples
25+
1026
## [0.26.0] - 2026-02-01
1127

1228
### Added
@@ -344,11 +360,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
344360
## [0.8.1] - 2026-01-11
345361

346362
### Documentation
363+
347364
- Simplify Claude Code MCP setup: use `claude mcp add` command instead of manual JSON configuration
348365

349366
## [0.8.0] - 2026-01-11
350367

351368
### Added
369+
352370
- **MCP Server Mode**: New `grepai mcp-serve` command for Model Context Protocol integration (#18)
353371
- Exposes grepai as native MCP tools for AI agents (Claude Code, Cursor, Windsurf, etc.)
354372
- Available tools: `grepai_search`, `grepai_trace_callers`, `grepai_trace_callees`, `grepai_trace_graph`, `grepai_index_status`
@@ -359,6 +377,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
359377
## [0.7.2] - 2026-01-11
360378

361379
### Documentation
380+
362381
- **Sidebar Reorganization**: Moved "Search Boost" and "Hybrid Search" from Configuration to Features section
363382
- **Configuration Reference**: Updated full configuration reference with correct field names
364383
- Added missing options: `version`, `watch.debounce_ms`, `trace.mode`, `trace.enabled_languages`, `trace.exclude_patterns`
@@ -370,6 +389,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
370389
## [0.7.1] - 2026-01-11
371390

372391
### Added
392+
373393
- **Agent Setup Trace Instructions**: Updated `grepai agent-setup` to include trace command documentation (#16)
374394
- Added "Call Graph Tracing" section with `trace callers`, `trace callees`, `trace graph` examples
375395
- All trace examples include `--json` flag for optimal AI agent integration
@@ -378,6 +398,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
378398
## [0.7.0] - 2026-01-10
379399

380400
### Added
401+
381402
- **Extended Language Support for Trace**: Symbol extraction now supports additional languages
382403
- C (`.c`, `.h`) - functions, structs, enums, typedefs
383404
- Zig (`.zig`) - functions, methods (inside structs/enums), inline/export/extern functions, structs, unions, enums, error sets, opaque types, nested types
@@ -388,6 +409,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
388409
## [0.6.0] - 2026-01-10
389410

390411
### Added
412+
391413
- **Search JSON Output**: New `--json`/`-j` flag for `grepai search` command
392414
- Machine-readable JSON output optimized for AI agents
393415
- Excludes internal fields (vector, hash, updated_at) to minimize token usage
@@ -397,6 +419,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
397419
## [0.5.0] - 2026-01-10
398420

399421
### Added
422+
400423
- **Call Graph Tracing**: New `grepai trace` command for code navigation
401424
- `trace callers <symbol>` - find all functions calling a symbol
402425
- `trace callees <symbol>` - find all functions called by a symbol
@@ -410,6 +433,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
410433
## [0.4.0] - 2026-01-10
411434

412435
### Added
436+
413437
- **LM Studio Provider**: New local embedding provider using LM Studio
414438
- Supports OpenAI-compatible API format
415439
- Configurable endpoint and model selection
@@ -418,6 +442,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
418442
## [0.3.0] - 2026-01-09
419443

420444
### Added
445+
421446
- **Search Boost**: Configurable score multipliers based on file paths
422447
- Penalize tests, mocks, fixtures, generated files, and docs
423448
- Boost source directories (`/src/`, `/lib/`, `/app/`)
@@ -431,11 +456,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
431456
- Feature cards on docs homepage
432457

433458
### Changed
459+
434460
- Searcher now accepts full SearchConfig instead of just BoostConfig
435461

436462
## [0.2.0] - 2026-01-09
437463

438464
### Added
465+
439466
- Initial release of grepai
440467
- `grepai init` command for project initialization
441468
- `grepai watch` command for real-time file indexing
@@ -452,12 +479,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
452479
- Cross-platform support (macOS, Linux, Windows)
453480

454481
### Security
482+
455483
- Privacy-first design with local embedding option
456484
- No telemetry or data collection
457485

458486
## [0.1.0] - 2026-01-09
459487

460488
### Added
489+
461490
- Initial public release
462491

463492
[Unreleased]: https://github.com/yoanbernabeu/grepai/compare/v0.26.0...HEAD

cli/mcp_serve.go

Lines changed: 83 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package cli
22

33
import (
44
"fmt"
5+
"os"
56
"path/filepath"
67

78
"github.com/spf13/cobra"
@@ -27,8 +28,13 @@ Arguments:
2728
project-path Optional path to the grepai project directory.
2829
If not provided, searches for .grepai from current directory.
2930
31+
Flags:
32+
--workspace Workspace name. When set, serves using workspace config from
33+
~/.grepai/workspace.yaml without requiring local .grepai/.
34+
3035
Configuration for Claude Code:
3136
claude mcp add grepai -- grepai mcp-serve
37+
claude mcp add grepai -- grepai mcp-serve --workspace myworkspace
3238
3339
Configuration for Cursor (.cursor/mcp.json):
3440
{
@@ -54,42 +60,96 @@ Configuration for Cursor with explicit path (recommended for Windows):
5460
}
5561

5662
func init() {
63+
mcpServeCmd.Flags().String("workspace", "", "Workspace name for workspace-only mode (no local .grepai/ required)")
5764
rootCmd.AddCommand(mcpServeCmd)
5865
}
5966

60-
func runMCPServe(_ *cobra.Command, args []string) error {
61-
var projectRoot string
62-
var err error
67+
// resolveMCPTarget determines the project root and/or workspace for the MCP server.
68+
// Returns (projectRoot, workspaceName, error).
69+
// projectRoot may be empty when in workspace-only mode.
70+
func resolveMCPTarget(explicitPath, workspaceName string) (string, string, error) {
71+
// Priority 1: Explicit --workspace flag
72+
if workspaceName != "" {
73+
cfg, err := config.LoadWorkspaceConfig()
74+
if err != nil {
75+
return "", "", fmt.Errorf("failed to load workspace config: %w", err)
76+
}
77+
if cfg == nil {
78+
return "", "", fmt.Errorf("no workspace config found at ~/.grepai/workspace.yaml")
79+
}
80+
if _, err := cfg.GetWorkspace(workspaceName); err != nil {
81+
return "", "", fmt.Errorf("workspace %q not found", workspaceName)
82+
}
83+
84+
// Check if cwd has local config (optional, for trace tools)
85+
projectRoot := ""
86+
if pr, err := config.FindProjectRoot(); err == nil {
87+
projectRoot = pr
88+
}
6389

64-
if len(args) > 0 {
65-
// Explicit path provided
66-
projectRoot = args[0]
90+
return projectRoot, workspaceName, nil
91+
}
6792

68-
// Convert to absolute path if relative
69-
if !filepath.IsAbs(projectRoot) {
70-
projectRoot, err = filepath.Abs(projectRoot)
93+
// Priority 2: Explicit project path argument
94+
if explicitPath != "" {
95+
if !filepath.IsAbs(explicitPath) {
96+
abs, err := filepath.Abs(explicitPath)
7197
if err != nil {
72-
return fmt.Errorf("failed to resolve path: %w", err)
98+
return "", "", fmt.Errorf("failed to resolve path: %w", err)
7399
}
100+
explicitPath = abs
74101
}
75-
76-
// Validate that it's a grepai project
77-
if !config.Exists(projectRoot) {
78-
return fmt.Errorf("no grepai project found at %s (run 'grepai init' first)", projectRoot)
79-
}
80-
} else {
81-
// Default behavior (backward compatibility)
82-
projectRoot, err = config.FindProjectRoot()
83-
if err != nil {
84-
return fmt.Errorf("failed to find project root: %w", err)
102+
if !config.Exists(explicitPath) {
103+
return "", "", fmt.Errorf("no grepai project found at %s (run 'grepai init' first)", explicitPath)
85104
}
105+
return explicitPath, "", nil
106+
}
107+
108+
// Priority 3: FindProjectRoot (walk upward from cwd)
109+
projectRoot, err := config.FindProjectRoot()
110+
if err == nil {
111+
return projectRoot, "", nil
86112
}
87113

88-
// Create and start MCP server
89-
server, err := mcp.NewServer(projectRoot)
114+
// Priority 4: Auto-detect workspace from cwd
115+
cwd, cwdErr := os.Getwd()
116+
if cwdErr != nil {
117+
return "", "", fmt.Errorf("failed to find project root: %w", err)
118+
}
119+
120+
wsName, ws, wsErr := config.FindWorkspaceForPath(cwd)
121+
if wsErr != nil {
122+
return "", "", fmt.Errorf("no grepai project or workspace found (run 'grepai init' or use --workspace)")
123+
}
124+
if ws != nil {
125+
return "", wsName, nil
126+
}
127+
128+
return "", "", fmt.Errorf("no grepai project or workspace found (run 'grepai init' or use --workspace)")
129+
}
130+
131+
func runMCPServe(cmd *cobra.Command, args []string) error {
132+
workspaceFlag, _ := cmd.Flags().GetString("workspace")
133+
134+
var explicitPath string
135+
if len(args) > 0 {
136+
explicitPath = args[0]
137+
}
138+
139+
projectRoot, wsName, err := resolveMCPTarget(explicitPath, workspaceFlag)
140+
if err != nil {
141+
return err
142+
}
143+
144+
var srv *mcp.Server
145+
if wsName != "" {
146+
srv, err = mcp.NewServerWithWorkspace(projectRoot, wsName)
147+
} else {
148+
srv, err = mcp.NewServer(projectRoot)
149+
}
90150
if err != nil {
91151
return fmt.Errorf("failed to create MCP server: %w", err)
92152
}
93153

94-
return server.Serve()
154+
return srv.Serve()
95155
}

cli/mcp_serve_test.go

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
package cli
2+
3+
import (
4+
"os"
5+
"path/filepath"
6+
"testing"
7+
8+
"github.com/yoanbernabeu/grepai/config"
9+
)
10+
11+
func TestResolveMCPWorkspace(t *testing.T) {
12+
t.Run("explicit_workspace_flag", func(t *testing.T) {
13+
tmpDir, _ := os.MkdirTemp("", "grepai-mcp-test")
14+
defer os.RemoveAll(tmpDir)
15+
cleanup := setTestHomeDirCLI(t, tmpDir)
16+
defer cleanup()
17+
18+
cfg := config.DefaultWorkspaceConfig()
19+
cfg.AddWorkspace(config.Workspace{
20+
Name: "orbix",
21+
Store: config.StoreConfig{Backend: "qdrant"},
22+
Embedder: config.EmbedderConfig{
23+
Provider: "ollama",
24+
Model: "nomic-embed-text",
25+
},
26+
Projects: []config.ProjectEntry{
27+
{Name: "pipeline", Path: filepath.Join(tmpDir, "pipeline")},
28+
},
29+
})
30+
config.SaveWorkspaceConfig(cfg)
31+
32+
projectRoot, wsName, err := resolveMCPTarget("", "orbix")
33+
if err != nil {
34+
t.Fatalf("unexpected error: %v", err)
35+
}
36+
if wsName != "orbix" {
37+
t.Errorf("expected workspace orbix, got %s", wsName)
38+
}
39+
_ = projectRoot
40+
})
41+
42+
t.Run("explicit_workspace_not_found", func(t *testing.T) {
43+
tmpDir, _ := os.MkdirTemp("", "grepai-mcp-test")
44+
defer os.RemoveAll(tmpDir)
45+
cleanup := setTestHomeDirCLI(t, tmpDir)
46+
defer cleanup()
47+
48+
_, _, err := resolveMCPTarget("", "nonexistent")
49+
if err == nil {
50+
t.Error("expected error for nonexistent workspace")
51+
}
52+
})
53+
54+
t.Run("explicit_project_path", func(t *testing.T) {
55+
tmpDir, _ := os.MkdirTemp("", "grepai-mcp-test")
56+
defer os.RemoveAll(tmpDir)
57+
58+
grepaiDir := filepath.Join(tmpDir, ".grepai")
59+
os.MkdirAll(grepaiDir, 0755)
60+
os.WriteFile(filepath.Join(grepaiDir, "config.yaml"), []byte("version: 1\n"), 0644)
61+
62+
projectRoot, wsName, err := resolveMCPTarget(tmpDir, "")
63+
if err != nil {
64+
t.Fatalf("unexpected error: %v", err)
65+
}
66+
if wsName != "" {
67+
t.Errorf("expected empty workspace name, got %s", wsName)
68+
}
69+
if projectRoot != tmpDir {
70+
t.Errorf("expected projectRoot %s, got %s", tmpDir, projectRoot)
71+
}
72+
})
73+
74+
t.Run("explicit_project_path_no_config", func(t *testing.T) {
75+
tmpDir, _ := os.MkdirTemp("", "grepai-mcp-test")
76+
defer os.RemoveAll(tmpDir)
77+
78+
_, _, err := resolveMCPTarget(tmpDir, "")
79+
if err == nil {
80+
t.Error("expected error when no .grepai/ at path")
81+
}
82+
})
83+
}

0 commit comments

Comments
 (0)