crowdmind 0.1.0__py3-none-any.whl

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.
crowdmind/__init__.py ADDED
@@ -0,0 +1,22 @@
1
+ """
2
+ CrowdMind - AI-Powered Research & Validation for Products
3
+
4
+ Validate your product ideas with simulated user panels,
5
+ research market pain points, and generate feature ideas.
6
+ """
7
+
8
+ __version__ = "0.1.0"
9
+
10
+ from crowdmind.validate.panel import run_evaluation as analyze
11
+ from crowdmind.validate.panel import run_evaluation as validate
12
+ from crowdmind.research.multi import run_multi_research as research
13
+ from crowdmind.validate.personas import Persona, PersonaPack
14
+
15
+ __all__ = [
16
+ "analyze",
17
+ "validate",
18
+ "research",
19
+ "Persona",
20
+ "PersonaPack",
21
+ "__version__",
22
+ ]
crowdmind/cli.py ADDED
@@ -0,0 +1,371 @@
1
+ """
2
+ CrowdMind CLI
3
+
4
+ A command-line interface for product research and validation.
5
+ """
6
+
7
+ import typer
8
+ from pathlib import Path
9
+ from typing import Optional, List
10
+ from rich.console import Console
11
+ from rich.table import Table
12
+ from rich.panel import Panel
13
+ from rich.progress import Progress, SpinnerColumn, TextColumn
14
+
15
+ app = typer.Typer(
16
+ name="crowdmind",
17
+ help="AI-Powered Research & Validation for Products",
18
+ no_args_is_help=True,
19
+ )
20
+
21
+ console = Console()
22
+
23
+
24
+ # Common options
25
+ def common_options(func):
26
+ """Decorator for common CLI options."""
27
+ func = typer.Option(None, "--personas", "-p", help="Number of personas to use")(func)
28
+ return func
29
+
30
+
31
+ @app.command()
32
+ def analyze(
33
+ path: Path = typer.Argument(..., help="Path to project or README file"),
34
+ personas: int = typer.Option(10, "--personas", "-p", help="Number of personas"),
35
+ pack: Optional[str] = typer.Option(None, "--pack", help="Persona pack (developers, enterprise, mixed)"),
36
+ users: Optional[List[str]] = typer.Option(None, "--users", "-u", help="Custom user types"),
37
+ provider: str = typer.Option("anthropic", "--provider", help="LLM provider"),
38
+ model: Optional[str] = typer.Option(None, "--model", "-m", help="Model to use"),
39
+ output: Optional[Path] = typer.Option(None, "--output", "-o", help="Output file"),
40
+ verbose: bool = typer.Option(False, "--verbose", "-v", help="Verbose output"),
41
+ quiet: bool = typer.Option(False, "--quiet", "-q", help="Minimal output"),
42
+ ):
43
+ """
44
+ Run full analysis pipeline on a project.
45
+
46
+ Includes: codebase analysis, research, ideation, and validation.
47
+ """
48
+ from crowdmind.research.codebase import run_analysis as analyze_codebase
49
+ from crowdmind.research.multi import run_multi_research
50
+ from crowdmind.ideate.features import run_ideation
51
+ from crowdmind.validate.panel import run_evaluation
52
+ from crowdmind.report.markdown import generate_report
53
+
54
+ if not quiet:
55
+ console.print(Panel.fit(
56
+ "[bold blue]CrowdMind Analysis[/bold blue]\n"
57
+ f"Project: {path}",
58
+ border_style="blue"
59
+ ))
60
+
61
+ results = {}
62
+
63
+ with Progress(
64
+ SpinnerColumn(),
65
+ TextColumn("[progress.description]{task.description}"),
66
+ console=console,
67
+ disable=quiet,
68
+ ) as progress:
69
+ # Step 1: Codebase analysis
70
+ task = progress.add_task("Analyzing codebase...", total=None)
71
+ results["codebase"] = analyze_codebase(path, verbose=verbose)
72
+ progress.update(task, completed=True)
73
+
74
+ # Step 2: Research
75
+ task = progress.add_task("Researching market...", total=None)
76
+ results["research"] = run_multi_research(use_cache=True, verbose=verbose)
77
+ progress.update(task, completed=True)
78
+
79
+ # Step 3: Ideation
80
+ task = progress.add_task("Generating ideas...", total=None)
81
+ results["ideation"] = run_ideation(num_ideas=5, verbose=verbose)
82
+ progress.update(task, completed=True)
83
+
84
+ # Step 4: Validation
85
+ task = progress.add_task("Validating with personas...", total=None)
86
+ readme_path = path / "README.md" if path.is_dir() else path
87
+ if readme_path.exists():
88
+ readme_content = readme_path.read_text()
89
+ results["validation"] = run_evaluation(
90
+ readme_content=readme_content,
91
+ verbose=verbose,
92
+ num_agents=personas,
93
+ )
94
+ progress.update(task, completed=True)
95
+
96
+ # Generate report
97
+ report = generate_report(results)
98
+
99
+ if output:
100
+ output.write_text(report)
101
+ if not quiet:
102
+ console.print(f"\n[green]Report saved to:[/green] {output}")
103
+ else:
104
+ console.print(report)
105
+
106
+ # Summary
107
+ if not quiet and results.get("validation"):
108
+ val = results["validation"]
109
+ console.print(Panel(
110
+ f"[bold]Star Rate:[/bold] {val.get('star_rate', 0)}%\n"
111
+ f"[bold]Avg Score:[/bold] {val.get('avg_star', 0)}/10\n"
112
+ f"[bold]Personas:[/bold] {val.get('agents_evaluated', 0)}",
113
+ title="Validation Summary",
114
+ border_style="green" if val.get('star_rate', 0) >= 50 else "yellow"
115
+ ))
116
+
117
+
118
+ @app.command()
119
+ def validate(
120
+ idea: str = typer.Argument(..., help="Idea or README content to validate"),
121
+ personas: int = typer.Option(10, "--personas", "-p", help="Number of personas"),
122
+ pack: Optional[str] = typer.Option(None, "--pack", help="Persona pack"),
123
+ categories: Optional[List[str]] = typer.Option(None, "--categories", "-c", help="Persona categories"),
124
+ provider: str = typer.Option("anthropic", "--provider", help="LLM provider"),
125
+ model: Optional[str] = typer.Option(None, "--model", "-m", help="Model to use"),
126
+ output: Optional[Path] = typer.Option(None, "--output", "-o", help="Output file (JSON)"),
127
+ verbose: bool = typer.Option(False, "--verbose", "-v", help="Verbose output"),
128
+ quiet: bool = typer.Option(False, "--quiet", "-q", help="Minimal output"),
129
+ ):
130
+ """
131
+ Validate a single idea with AI personas.
132
+
133
+ Pass a product description, README content, or path to README file.
134
+ """
135
+ from crowdmind.validate.panel import run_evaluation
136
+ import json
137
+
138
+ # Check if idea is a file path
139
+ idea_path = Path(idea)
140
+ if idea_path.exists():
141
+ content = idea_path.read_text()
142
+ else:
143
+ content = idea
144
+
145
+ if not quiet:
146
+ console.print(Panel.fit(
147
+ "[bold blue]CrowdMind Validation[/bold blue]\n"
148
+ f"Personas: {personas}",
149
+ border_style="blue"
150
+ ))
151
+
152
+ result = run_evaluation(
153
+ readme_content=content,
154
+ verbose=verbose and not quiet,
155
+ num_agents=personas,
156
+ categories=categories,
157
+ )
158
+
159
+ if output:
160
+ output.write_text(json.dumps(result, indent=2))
161
+ if not quiet:
162
+ console.print(f"\n[green]Results saved to:[/green] {output}")
163
+
164
+ # Display results
165
+ if not quiet:
166
+ table = Table(title="Validation Results")
167
+ table.add_column("Metric", style="cyan")
168
+ table.add_column("Value", style="green")
169
+
170
+ table.add_row("Star Rate", f"{result.get('star_rate', 0)}%")
171
+ table.add_row("Avg Star Likelihood", f"{result.get('avg_star', 0)}/10")
172
+ table.add_row("Total Score", f"{result.get('total_score', 0)}/100")
173
+ table.add_row("Personas Evaluated", str(result.get("agents_evaluated", 0)))
174
+
175
+ console.print(table)
176
+
177
+ # Category breakdown
178
+ if result.get("by_category"):
179
+ console.print("\n[bold]By Category:[/bold]")
180
+ for cat, data in sorted(result["by_category"].items(), key=lambda x: x[1]["avg"], reverse=True):
181
+ console.print(f" {cat}: {data['avg']}/10")
182
+
183
+
184
+ @app.command()
185
+ def research(
186
+ path: Optional[Path] = typer.Argument(None, help="Path to project (optional)"),
187
+ sources: List[str] = typer.Option(
188
+ ["reddit", "hackernews", "github"],
189
+ "--sources", "-s",
190
+ help="Sources to research"
191
+ ),
192
+ topics: Optional[List[str]] = typer.Option(None, "--topics", "-t", help="Custom topics to search"),
193
+ no_cache: bool = typer.Option(False, "--no-cache", help="Skip cache"),
194
+ output: Optional[Path] = typer.Option(None, "--output", "-o", help="Output file"),
195
+ verbose: bool = typer.Option(False, "--verbose", "-v", help="Verbose output"),
196
+ quiet: bool = typer.Option(False, "--quiet", "-q", help="Minimal output"),
197
+ ):
198
+ """
199
+ Research market pain points from multiple sources.
200
+
201
+ Sources: reddit, hackernews, github
202
+ """
203
+ from crowdmind.research.multi import run_multi_research, get_multi_research_summary
204
+ import json
205
+
206
+ if not quiet:
207
+ console.print(Panel.fit(
208
+ "[bold blue]CrowdMind Research[/bold blue]\n"
209
+ f"Sources: {', '.join(sources)}",
210
+ border_style="blue"
211
+ ))
212
+
213
+ result = run_multi_research(
214
+ use_cache=not no_cache,
215
+ verbose=verbose and not quiet,
216
+ sources=sources,
217
+ )
218
+
219
+ if output:
220
+ output.write_text(json.dumps(result, indent=2))
221
+ if not quiet:
222
+ console.print(f"\n[green]Results saved to:[/green] {output}")
223
+
224
+ # Display summary
225
+ if not quiet:
226
+ summary = get_multi_research_summary()
227
+ console.print(summary)
228
+
229
+
230
+ @app.command()
231
+ def market(
232
+ path: Path = typer.Argument(..., help="Path to project or README"),
233
+ personas: int = typer.Option(10, "--personas", "-p", help="Number of buyer personas"),
234
+ no_cache: bool = typer.Option(False, "--no-cache", help="Skip cache"),
235
+ output: Optional[Path] = typer.Option(None, "--output", "-o", help="Output file"),
236
+ verbose: bool = typer.Option(False, "--verbose", "-v", help="Verbose output"),
237
+ quiet: bool = typer.Option(False, "--quiet", "-q", help="Minimal output"),
238
+ ):
239
+ """
240
+ Run market analysis on a project.
241
+
242
+ Includes: buyer personas, pricing analysis, success predictions.
243
+ """
244
+ from crowdmind.market.analysis import run_full_market_analysis, get_market_summary
245
+ import json
246
+
247
+ if not quiet:
248
+ console.print(Panel.fit(
249
+ "[bold blue]CrowdMind Market Analysis[/bold blue]\n"
250
+ f"Project: {path}",
251
+ border_style="blue"
252
+ ))
253
+
254
+ result = run_full_market_analysis(
255
+ project_path=path,
256
+ use_cache=not no_cache,
257
+ verbose=verbose and not quiet,
258
+ )
259
+
260
+ if output:
261
+ output.write_text(json.dumps(result, indent=2))
262
+ if not quiet:
263
+ console.print(f"\n[green]Results saved to:[/green] {output}")
264
+
265
+ if not quiet:
266
+ summary = get_market_summary()
267
+ console.print(summary)
268
+
269
+
270
+ @app.command()
271
+ def personas():
272
+ """
273
+ Interactive wizard to create custom personas.
274
+
275
+ [Stub - Coming soon]
276
+ """
277
+ from crowdmind.validate.personas import PERSONA_PACKS, Persona
278
+
279
+ console.print(Panel.fit(
280
+ "[bold blue]CrowdMind Personas[/bold blue]\n"
281
+ "Interactive persona creation wizard",
282
+ border_style="blue"
283
+ ))
284
+
285
+ console.print("\n[bold]Available Persona Packs:[/bold]")
286
+ for name, pack in PERSONA_PACKS.items():
287
+ console.print(f" [cyan]{name}[/cyan]: {pack.description} ({len(pack.personas)} personas)")
288
+
289
+ console.print("\n[yellow]Interactive wizard coming soon![/yellow]")
290
+ console.print("For now, use --pack option with validate/analyze commands.")
291
+
292
+
293
+ @app.command()
294
+ def demo():
295
+ """
296
+ Show a demo of CrowdMind capabilities.
297
+
298
+ [Stub - Coming soon]
299
+ """
300
+ console.print(Panel.fit(
301
+ "[bold blue]CrowdMind Demo[/bold blue]\n"
302
+ "Interactive demonstration",
303
+ border_style="blue"
304
+ ))
305
+
306
+ console.print("\n[yellow]Demo mode coming soon![/yellow]")
307
+ console.print("\nTry these commands instead:")
308
+ console.print(" [cyan]crowdmind validate \"My SaaS idea description\"[/cyan]")
309
+ console.print(" [cyan]crowdmind research --sources reddit hackernews[/cyan]")
310
+ console.print(" [cyan]crowdmind analyze ./my-project[/cyan]")
311
+
312
+
313
+ @app.command()
314
+ def config():
315
+ """
316
+ Setup wizard for CrowdMind configuration.
317
+
318
+ [Stub - Coming soon]
319
+ """
320
+ from crowdmind.config import CONFIG_FILE, get_config
321
+
322
+ console.print(Panel.fit(
323
+ "[bold blue]CrowdMind Configuration[/bold blue]",
324
+ border_style="blue"
325
+ ))
326
+
327
+ cfg = get_config()
328
+
329
+ console.print(f"\n[bold]Config file:[/bold] {CONFIG_FILE}")
330
+ console.print(f"[bold]Config exists:[/bold] {CONFIG_FILE.exists()}")
331
+
332
+ console.print("\n[bold]Current Settings:[/bold]")
333
+ console.print(f" Default model: {cfg.default_model}")
334
+ console.print(f" Default personas: {cfg.default_personas}")
335
+ console.print(f" Default provider: {cfg.default_provider}")
336
+ console.print(f" Cache dir: {cfg.cache_dir}")
337
+
338
+ console.print("\n[bold]API Keys:[/bold]")
339
+ console.print(f" OpenAI: {'[green]configured[/green]' if cfg.openai_api_key else '[red]not set[/red]'}")
340
+ console.print(f" Anthropic: {'[green]configured[/green]' if cfg.anthropic_api_key else '[red]not set[/red]'}")
341
+ console.print(f" Google: {'[green]configured[/green]' if cfg.google_api_key else '[red]not set[/red]'}")
342
+ console.print(f" Groq: {'[green]configured[/green]' if cfg.groq_api_key else '[red]not set[/red]'}")
343
+ console.print(f" GitHub: {'[green]configured[/green]' if cfg.github_token else '[red]not set[/red]'}")
344
+
345
+ console.print("\n[yellow]Interactive setup wizard coming soon![/yellow]")
346
+ console.print("\nFor now, set environment variables:")
347
+ console.print(" export ANTHROPIC_API_KEY=your-key")
348
+ console.print(" export CROWDMIND_MODEL=claude-sonnet-4-6")
349
+ console.print(" export CROWDMIND_PERSONAS=15")
350
+
351
+
352
+ @app.callback()
353
+ def main_callback(
354
+ version: bool = typer.Option(False, "--version", "-V", help="Show version"),
355
+ ):
356
+ """
357
+ CrowdMind - AI-Powered Research & Validation for Products
358
+ """
359
+ if version:
360
+ from crowdmind import __version__
361
+ console.print(f"crowdmind version {__version__}")
362
+ raise typer.Exit()
363
+
364
+
365
+ def main():
366
+ """Entry point for the CLI."""
367
+ app()
368
+
369
+
370
+ if __name__ == "__main__":
371
+ main()
crowdmind/config.py ADDED
@@ -0,0 +1,177 @@
1
+ """
2
+ CrowdMind Configuration
3
+
4
+ Loads configuration from:
5
+ 1. ~/.crowdmind/config.yaml
6
+ 2. Environment variables
7
+ """
8
+
9
+ import os
10
+ from pathlib import Path
11
+ from typing import Optional, Dict, Any
12
+ from dataclasses import dataclass, field
13
+
14
+ try:
15
+ import yaml
16
+ HAS_YAML = True
17
+ except ImportError:
18
+ HAS_YAML = False
19
+
20
+
21
+ CONFIG_DIR = Path.home() / ".crowdmind"
22
+ CONFIG_FILE = CONFIG_DIR / "config.yaml"
23
+
24
+
25
+ @dataclass
26
+ class CrowdMindConfig:
27
+ """Configuration for CrowdMind."""
28
+
29
+ # API Keys
30
+ openai_api_key: Optional[str] = None
31
+ anthropic_api_key: Optional[str] = None
32
+ google_api_key: Optional[str] = None
33
+ groq_api_key: Optional[str] = None
34
+ github_token: Optional[str] = None
35
+
36
+ # Default settings
37
+ default_model: str = "claude-sonnet-4-6"
38
+ default_personas: int = 10
39
+ default_provider: str = "anthropic"
40
+
41
+ # Cache settings
42
+ cache_dir: Path = field(default_factory=lambda: CONFIG_DIR / "cache")
43
+ cache_hours: int = 12
44
+
45
+ # Output settings
46
+ output_dir: Path = field(default_factory=lambda: Path.cwd() / "crowdmind_output")
47
+
48
+ # Research settings
49
+ reddit_subreddits: list = field(default_factory=lambda: [
50
+ "programming", "webdev", "devops", "startups", "SaaS"
51
+ ])
52
+
53
+ @classmethod
54
+ def load(cls) -> "CrowdMindConfig":
55
+ """Load configuration from file and environment variables."""
56
+ config = cls()
57
+
58
+ # Load from YAML if available
59
+ if HAS_YAML and CONFIG_FILE.exists():
60
+ try:
61
+ with open(CONFIG_FILE) as f:
62
+ yaml_config = yaml.safe_load(f) or {}
63
+ config = cls._from_dict(yaml_config)
64
+ except Exception:
65
+ pass
66
+
67
+ # Override with environment variables
68
+ config.openai_api_key = os.getenv("OPENAI_API_KEY", config.openai_api_key)
69
+ config.anthropic_api_key = os.getenv("ANTHROPIC_API_KEY", config.anthropic_api_key)
70
+ config.google_api_key = os.getenv("GOOGLE_API_KEY", config.google_api_key)
71
+ config.groq_api_key = os.getenv("GROQ_API_KEY", config.groq_api_key)
72
+ config.github_token = os.getenv("GITHUB_TOKEN", config.github_token)
73
+
74
+ # CrowdMind-specific env vars
75
+ if os.getenv("CROWDMIND_MODEL"):
76
+ config.default_model = os.getenv("CROWDMIND_MODEL")
77
+ if os.getenv("CROWDMIND_PERSONAS"):
78
+ try:
79
+ config.default_personas = int(os.getenv("CROWDMIND_PERSONAS"))
80
+ except ValueError:
81
+ pass
82
+ if os.getenv("CROWDMIND_PROVIDER"):
83
+ config.default_provider = os.getenv("CROWDMIND_PROVIDER")
84
+
85
+ return config
86
+
87
+ @classmethod
88
+ def _from_dict(cls, data: Dict[str, Any]) -> "CrowdMindConfig":
89
+ """Create config from dictionary."""
90
+ config = cls()
91
+
92
+ # API keys
93
+ api_keys = data.get("api_keys", {})
94
+ config.openai_api_key = api_keys.get("openai")
95
+ config.anthropic_api_key = api_keys.get("anthropic")
96
+ config.google_api_key = api_keys.get("google")
97
+ config.groq_api_key = api_keys.get("groq")
98
+ config.github_token = api_keys.get("github")
99
+
100
+ # Defaults
101
+ defaults = data.get("defaults", {})
102
+ config.default_model = defaults.get("model", config.default_model)
103
+ config.default_personas = defaults.get("personas", config.default_personas)
104
+ config.default_provider = defaults.get("provider", config.default_provider)
105
+
106
+ # Cache
107
+ cache = data.get("cache", {})
108
+ if cache.get("dir"):
109
+ config.cache_dir = Path(cache["dir"]).expanduser()
110
+ config.cache_hours = cache.get("hours", config.cache_hours)
111
+
112
+ # Output
113
+ if data.get("output_dir"):
114
+ config.output_dir = Path(data["output_dir"]).expanduser()
115
+
116
+ # Research
117
+ research = data.get("research", {})
118
+ if research.get("subreddits"):
119
+ config.reddit_subreddits = research["subreddits"]
120
+
121
+ return config
122
+
123
+ def save(self) -> None:
124
+ """Save configuration to YAML file."""
125
+ if not HAS_YAML:
126
+ raise ImportError("PyYAML is required to save config. Install with: pip install pyyaml")
127
+
128
+ CONFIG_DIR.mkdir(parents=True, exist_ok=True)
129
+
130
+ data = {
131
+ "api_keys": {
132
+ "openai": self.openai_api_key,
133
+ "anthropic": self.anthropic_api_key,
134
+ "google": self.google_api_key,
135
+ "groq": self.groq_api_key,
136
+ "github": self.github_token,
137
+ },
138
+ "defaults": {
139
+ "model": self.default_model,
140
+ "personas": self.default_personas,
141
+ "provider": self.default_provider,
142
+ },
143
+ "cache": {
144
+ "dir": str(self.cache_dir),
145
+ "hours": self.cache_hours,
146
+ },
147
+ "output_dir": str(self.output_dir),
148
+ "research": {
149
+ "subreddits": self.reddit_subreddits,
150
+ },
151
+ }
152
+
153
+ with open(CONFIG_FILE, "w") as f:
154
+ yaml.dump(data, f, default_flow_style=False)
155
+
156
+ def ensure_dirs(self) -> None:
157
+ """Ensure required directories exist."""
158
+ self.cache_dir.mkdir(parents=True, exist_ok=True)
159
+ self.output_dir.mkdir(parents=True, exist_ok=True)
160
+
161
+
162
+ def get_config() -> CrowdMindConfig:
163
+ """Get the current configuration."""
164
+ return CrowdMindConfig.load()
165
+
166
+
167
+ def get_api_key(provider: str) -> Optional[str]:
168
+ """Get API key for a provider."""
169
+ config = get_config()
170
+ key_map = {
171
+ "openai": config.openai_api_key,
172
+ "anthropic": config.anthropic_api_key,
173
+ "google": config.google_api_key,
174
+ "groq": config.groq_api_key,
175
+ "github": config.github_token,
176
+ }
177
+ return key_map.get(provider.lower())
@@ -0,0 +1,13 @@
1
+ """
2
+ CrowdMind Ideate Module
3
+
4
+ Generate feature ideas based on research and codebase analysis.
5
+ """
6
+
7
+ from crowdmind.ideate.features import run_ideation, generate_feature_ideas, get_top_ideas
8
+
9
+ __all__ = [
10
+ "run_ideation",
11
+ "generate_feature_ideas",
12
+ "get_top_ideas",
13
+ ]