ycb 3.4.2__tar.gz → 3.4.3__tar.gz

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.
Files changed (75) hide show
  1. {ycb-3.4.2 → ycb-3.4.3}/PKG-INFO +7 -4
  2. {ycb-3.4.2 → ycb-3.4.3}/README.md +6 -3
  3. {ycb-3.4.2 → ycb-3.4.3}/pyproject.toml +1 -1
  4. {ycb-3.4.2 → ycb-3.4.3}/src/__init__.py +1 -1
  5. {ycb-3.4.2 → ycb-3.4.3}/src/cli/main.py +133 -31
  6. {ycb-3.4.2 → ycb-3.4.3}/src/licensing/tier_mapping.py +104 -10
  7. {ycb-3.4.2 → ycb-3.4.3}/.env.example +0 -0
  8. {ycb-3.4.2 → ycb-3.4.3}/.gitignore +0 -0
  9. {ycb-3.4.2 → ycb-3.4.3}/src/cli/__init__.py +0 -0
  10. {ycb-3.4.2 → ycb-3.4.3}/src/cli/ui/__init__.py +0 -0
  11. {ycb-3.4.2 → ycb-3.4.3}/src/cli/ui/animation.py +0 -0
  12. {ycb-3.4.2 → ycb-3.4.3}/src/cli/ui/components.py +0 -0
  13. {ycb-3.4.2 → ycb-3.4.3}/src/cli/ui/dashboard.py +0 -0
  14. {ycb-3.4.2 → ycb-3.4.3}/src/cli/ui/logger.py +0 -0
  15. {ycb-3.4.2 → ycb-3.4.3}/src/config/__init__.py +0 -0
  16. {ycb-3.4.2 → ycb-3.4.3}/src/connectors/__init__.py +0 -0
  17. {ycb-3.4.2 → ycb-3.4.3}/src/connectors/airtable_connector.py +0 -0
  18. {ycb-3.4.2 → ycb-3.4.3}/src/connectors/analytics_connector.py +0 -0
  19. {ycb-3.4.2 → ycb-3.4.3}/src/connectors/asana_connector.py +0 -0
  20. {ycb-3.4.2 → ycb-3.4.3}/src/connectors/ashby_connector.py +0 -0
  21. {ycb-3.4.2 → ycb-3.4.3}/src/connectors/bamboohr_connector.py +0 -0
  22. {ycb-3.4.2 → ycb-3.4.3}/src/connectors/base_connector.py +0 -0
  23. {ycb-3.4.2 → ycb-3.4.3}/src/connectors/basecamp_connector.py +0 -0
  24. {ycb-3.4.2 → ycb-3.4.3}/src/connectors/calendly_connector.py +0 -0
  25. {ycb-3.4.2 → ycb-3.4.3}/src/connectors/clickup_connector.py +0 -0
  26. {ycb-3.4.2 → ycb-3.4.3}/src/connectors/datadog_connector.py +0 -0
  27. {ycb-3.4.2 → ycb-3.4.3}/src/connectors/deel_connector.py +0 -0
  28. {ycb-3.4.2 → ycb-3.4.3}/src/connectors/discord_connector.py +0 -0
  29. {ycb-3.4.2 → ycb-3.4.3}/src/connectors/dropbox_connector.py +0 -0
  30. {ycb-3.4.2 → ycb-3.4.3}/src/connectors/facebook_connector.py +0 -0
  31. {ycb-3.4.2 → ycb-3.4.3}/src/connectors/front_connector.py +0 -0
  32. {ycb-3.4.2 → ycb-3.4.3}/src/connectors/github_connector.py +0 -0
  33. {ycb-3.4.2 → ycb-3.4.3}/src/connectors/gitlab_connector.py +0 -0
  34. {ycb-3.4.2 → ycb-3.4.3}/src/connectors/gmail_connector.py +0 -0
  35. {ycb-3.4.2 → ycb-3.4.3}/src/connectors/google_docs_connector.py +0 -0
  36. {ycb-3.4.2 → ycb-3.4.3}/src/connectors/google_drive_connector.py +0 -0
  37. {ycb-3.4.2 → ycb-3.4.3}/src/connectors/google_sheets_connector.py +0 -0
  38. {ycb-3.4.2 → ycb-3.4.3}/src/connectors/hubspot_connector.py +0 -0
  39. {ycb-3.4.2 → ycb-3.4.3}/src/connectors/jira_connector.py +0 -0
  40. {ycb-3.4.2 → ycb-3.4.3}/src/connectors/linear_connector.py +0 -0
  41. {ycb-3.4.2 → ycb-3.4.3}/src/connectors/monday_connector.py +0 -0
  42. {ycb-3.4.2 → ycb-3.4.3}/src/connectors/notion_connector.py +0 -0
  43. {ycb-3.4.2 → ycb-3.4.3}/src/connectors/outlook_teams_connector.py +0 -0
  44. {ycb-3.4.2 → ycb-3.4.3}/src/connectors/registry.py +0 -0
  45. {ycb-3.4.2 → ycb-3.4.3}/src/connectors/rippling_connector.py +0 -0
  46. {ycb-3.4.2 → ycb-3.4.3}/src/connectors/salesforce_connector.py +0 -0
  47. {ycb-3.4.2 → ycb-3.4.3}/src/connectors/search_connector.py +0 -0
  48. {ycb-3.4.2 → ycb-3.4.3}/src/connectors/segment_connector.py +0 -0
  49. {ycb-3.4.2 → ycb-3.4.3}/src/connectors/slack_connector.py +0 -0
  50. {ycb-3.4.2 → ycb-3.4.3}/src/connectors/slack_oauth.py +0 -0
  51. {ycb-3.4.2 → ycb-3.4.3}/src/connectors/todoist_connector.py +0 -0
  52. {ycb-3.4.2 → ycb-3.4.3}/src/connectors/twitter_connector.py +0 -0
  53. {ycb-3.4.2 → ycb-3.4.3}/src/connectors/web_crawler_connector.py +0 -0
  54. {ycb-3.4.2 → ycb-3.4.3}/src/connectors/zoom_connector.py +0 -0
  55. {ycb-3.4.2 → ycb-3.4.3}/src/licensing/__init__.py +0 -0
  56. {ycb-3.4.2 → ycb-3.4.3}/src/licensing/validator.py +0 -0
  57. {ycb-3.4.2 → ycb-3.4.3}/src/models/__init__.py +0 -0
  58. {ycb-3.4.2 → ycb-3.4.3}/src/models/domain.py +0 -0
  59. {ycb-3.4.2 → ycb-3.4.3}/src/processors/__init__.py +0 -0
  60. {ycb-3.4.2 → ycb-3.4.3}/src/processors/chunker.py +0 -0
  61. {ycb-3.4.2 → ycb-3.4.3}/src/processors/knowledge_extractor.py +0 -0
  62. {ycb-3.4.2 → ycb-3.4.3}/src/processors/skills_synthesizer.py +0 -0
  63. {ycb-3.4.2 → ycb-3.4.3}/src/qa/__init__.py +0 -0
  64. {ycb-3.4.2 → ycb-3.4.3}/src/qa/ask_engine.py +0 -0
  65. {ycb-3.4.2 → ycb-3.4.3}/src/qa/providers/__init__.py +0 -0
  66. {ycb-3.4.2 → ycb-3.4.3}/src/qa/providers/anthropic_provider.py +0 -0
  67. {ycb-3.4.2 → ycb-3.4.3}/src/qa/providers/base.py +0 -0
  68. {ycb-3.4.2 → ycb-3.4.3}/src/qa/providers/ollama_provider.py +0 -0
  69. {ycb-3.4.2 → ycb-3.4.3}/src/qa/providers/openai_provider.py +0 -0
  70. {ycb-3.4.2 → ycb-3.4.3}/src/qa/providers/openrouter_provider.py +0 -0
  71. {ycb-3.4.2 → ycb-3.4.3}/src/storage/__init__.py +0 -0
  72. {ycb-3.4.2 → ycb-3.4.3}/src/storage/database.py +0 -0
  73. {ycb-3.4.2 → ycb-3.4.3}/src/storage/storage_manager.py +0 -0
  74. {ycb-3.4.2 → ycb-3.4.3}/src/sync/__init__.py +0 -0
  75. {ycb-3.4.2 → ycb-3.4.3}/src/sync/sync_scheduler.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: ycb
3
- Version: 3.4.2
3
+ Version: 3.4.3
4
4
  Summary: Your Company Brain: Automated local knowledge extraction pipeline converting docs & threads into AI agent skills
5
5
  Requires-Python: >=3.11
6
6
  Requires-Dist: anthropic>=0.40
@@ -19,14 +19,17 @@ Requires-Dist: slack-sdk
19
19
  Requires-Dist: sqlalchemy>=2.0
20
20
  Description-Content-Type: text/markdown
21
21
 
22
- # 🧠 Your Company Brain (v3.4.2)
22
+ # 🧠 Your Company Brain (v3.4.3)
23
23
 
24
24
  **Your Company Brain** is an automated, offline-first knowledge extraction pipeline and multi-provider AI Q&A system. It connects to **38 connectors** across your company's communication, engineering, HR, productivity, customer support, and analytics stack, pulls all scattered information, runs it through local or cloud LLMs, and outputs structured "Skills" — machine-readable procedure cards that AI agents can directly execute, or lets you query your company data directly in your terminal using natural-language search (`ycb --ask`).
25
25
 
26
26
  Instead of manually writing instruction manuals for your AI tools or searching across 10 different dashboards, Your Company Brain generates them automatically and answers questions across any model you choose.
27
27
 
28
- > 🚀 **What's New in v3.4.2 — Max Subscription Tier, Self-Healing Licensing, Dedicated Auth & Windows ASCII UI**:
29
- > - **Max Monthly Subscription Tier**: New `"max"` tier unlocks all **38 connectors** with automated 30-day subscription management.
28
+ > 🚀 **What's New in v3.4.3 — Guided Connector Setup, Sync Onboarding & API Key Management**:
29
+ > - **Guided `ycb connect` with Credential Links**: Every credential prompt now shows exactly where to get the key — direct link to the platform's dashboard (e.g. `https://github.com/settings/tokens` for GitHub, `https://linear.app/settings/api` for Linear) across all 38 connectors.
30
+ > - **Smart `ycb sync` Onboarding**: When no connectors are configured, `ycb sync` now shows a clean panel with tier-specific `ycb connect` examples instead of launching an empty idle dashboard.
31
+ > - **`ycb switch model --update-key`**: New flag to re-enter your API key for the current provider. Switching to a different provider (e.g. OpenAI → Anthropic) automatically re-prompts for the new key.
32
+ > - **Max Monthly Subscription Tier**: `"max"` tier unlocks all **38 connectors** with automated 30-day subscription management and background self-healing.
30
33
  > - **Self-Healing License Synchronization**: When key or email updates occur in `.env`, the CLI automatically validates and heals local cache in the background with zero manual re-login required.
31
34
  > - **Automated Occasional Expiry & Freshness Check**: Verifies license validity every 24 hours and upon subscription expiration with a 72-hour offline grace period and clean renewal notices.
32
35
  > - **Dedicated Auth Commands (`ycb login` & `ycb logout`)**: Easily switch accounts, authenticate with license key and registered email, and clear local sessions securely.
@@ -1,11 +1,14 @@
1
- # 🧠 Your Company Brain (v3.4.2)
1
+ # 🧠 Your Company Brain (v3.4.3)
2
2
 
3
3
  **Your Company Brain** is an automated, offline-first knowledge extraction pipeline and multi-provider AI Q&A system. It connects to **38 connectors** across your company's communication, engineering, HR, productivity, customer support, and analytics stack, pulls all scattered information, runs it through local or cloud LLMs, and outputs structured "Skills" — machine-readable procedure cards that AI agents can directly execute, or lets you query your company data directly in your terminal using natural-language search (`ycb --ask`).
4
4
 
5
5
  Instead of manually writing instruction manuals for your AI tools or searching across 10 different dashboards, Your Company Brain generates them automatically and answers questions across any model you choose.
6
6
 
7
- > 🚀 **What's New in v3.4.2 — Max Subscription Tier, Self-Healing Licensing, Dedicated Auth & Windows ASCII UI**:
8
- > - **Max Monthly Subscription Tier**: New `"max"` tier unlocks all **38 connectors** with automated 30-day subscription management.
7
+ > 🚀 **What's New in v3.4.3 — Guided Connector Setup, Sync Onboarding & API Key Management**:
8
+ > - **Guided `ycb connect` with Credential Links**: Every credential prompt now shows exactly where to get the key — direct link to the platform's dashboard (e.g. `https://github.com/settings/tokens` for GitHub, `https://linear.app/settings/api` for Linear) across all 38 connectors.
9
+ > - **Smart `ycb sync` Onboarding**: When no connectors are configured, `ycb sync` now shows a clean panel with tier-specific `ycb connect` examples instead of launching an empty idle dashboard.
10
+ > - **`ycb switch model --update-key`**: New flag to re-enter your API key for the current provider. Switching to a different provider (e.g. OpenAI → Anthropic) automatically re-prompts for the new key.
11
+ > - **Max Monthly Subscription Tier**: `"max"` tier unlocks all **38 connectors** with automated 30-day subscription management and background self-healing.
9
12
  > - **Self-Healing License Synchronization**: When key or email updates occur in `.env`, the CLI automatically validates and heals local cache in the background with zero manual re-login required.
10
13
  > - **Automated Occasional Expiry & Freshness Check**: Verifies license validity every 24 hours and upon subscription expiration with a 72-hour offline grace period and clean renewal notices.
11
14
  > - **Dedicated Auth Commands (`ycb login` & `ycb logout`)**: Easily switch accounts, authenticate with license key and registered email, and clear local sessions securely.
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "ycb"
7
- version = "3.4.2"
7
+ version = "3.4.3"
8
8
  description = "Your Company Brain: Automated local knowledge extraction pipeline converting docs & threads into AI agent skills"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -1,2 +1,2 @@
1
1
  """Company Brain package root"""
2
- __version__ = "3.4.2"
2
+ __version__ = "3.4.3"
@@ -19,6 +19,7 @@ from src.licensing.tier_mapping import (
19
19
  TIER_CONNECTORS,
20
20
  CONNECTOR_CATEGORIES,
21
21
  CONNECTOR_ENV_VARS,
22
+ CONNECTOR_CREDENTIAL_LINKS,
22
23
  get_cached_plan_tier,
23
24
  is_connector_permitted,
24
25
  save_connector_credentials,
@@ -240,6 +241,39 @@ def sync(days, once, interactive, plain, connector):
240
241
  else:
241
242
  target_connectors = None
242
243
 
244
+ # ── Pre-flight connector check ────────────────────────────────────────────
245
+ # Resolve enabled connectors once here so we can give immediate feedback.
246
+ # We re-use the same list for the scheduler below.
247
+ enabled_connectors = get_enabled_connectors(target_connectors=target_connectors, plan_tier=plan_tier)
248
+
249
+ if not enabled_connectors:
250
+ # Suggest the first few connectors allowed on the user's tier
251
+ tier_connectors = TIER_CONNECTORS.get(plan_tier.lower(), ["google_docs", "slack"])
252
+ examples = tier_connectors[:3] if tier_connectors else ["google_docs", "slack", "github"]
253
+ example_lines = "\n".join(f" [cyan]ycb connect {c}[/cyan]" for c in examples)
254
+
255
+ console.print(
256
+ Panel.fit(
257
+ "[bold yellow]No connectors are currently connected or configured.[/bold yellow]\n\n"
258
+ "To ingest and sync your company knowledge, connect at least one platform:\n"
259
+ " [bold cyan]ycb connect <connector_name>[/bold cyan]\n\n"
260
+ f"[bold]Available in your {plan_tier.title()} plan:[/bold]\n"
261
+ f"{example_lines}\n\n"
262
+ "Run [bold cyan]ycb status[/bold cyan] to see all available connectors for your plan tier.",
263
+ title="[bold cyan]Company Brain — Sync[/bold cyan]",
264
+ border_style="yellow",
265
+ )
266
+ )
267
+ return
268
+
269
+ # Show a brief summary of what will be synced
270
+ connector_names = [c.get_source_name() for c in enabled_connectors]
271
+ console.print(
272
+ f"[bold green][OK][/bold green] Connected applications "
273
+ f"({len(connector_names)}): [bold cyan]{', '.join(connector_names)}[/bold cyan]\n"
274
+ )
275
+ # ─────────────────────────────────────────────────────────────────────────
276
+
243
277
  scheduler = SyncScheduler(sync_interval_minutes=30)
244
278
 
245
279
  # Setup UI state and logging
@@ -314,15 +348,25 @@ def connect(connector_name):
314
348
  return
315
349
 
316
350
  if name == "slack":
351
+ console.print()
352
+ console.print(
353
+ "[bold]Slack requires a Slack App with OAuth enabled.[/bold]\n"
354
+ " [dim]Where to get credentials:[/dim] "
355
+ "[cyan]https://api.slack.com/apps[/cyan]\n"
356
+ " 1. Create a new Slack App (or use an existing one)\n"
357
+ " 2. Under [bold]OAuth & Permissions[/bold], add the user token scopes you need\n"
358
+ " 3. Copy the [bold]Client ID[/bold] and [bold]Client Secret[/bold] from [bold]Basic Information[/bold]"
359
+ )
360
+ console.print()
317
361
  client_id = os.getenv("SLACK_CLIENT_ID", "")
318
362
  client_secret = os.getenv("SLACK_CLIENT_SECRET", "")
319
363
 
320
364
  if not client_id:
321
- console.print("[cyan]Admin Setup: SLACK_CLIENT_ID not found in config.[/cyan]")
365
+ console.print("[cyan]SLACK_CLIENT_ID not found in config.[/cyan]")
322
366
  client_id = click.prompt("Enter Slack App Client ID (SLACK_CLIENT_ID)").strip()
323
367
 
324
368
  if not client_secret:
325
- console.print("[cyan]Admin Setup: SLACK_CLIENT_SECRET not found in config.[/cyan]")
369
+ console.print("[cyan]SLACK_CLIENT_SECRET not found in config.[/cyan]")
326
370
  client_secret = click.prompt("Enter Slack App Client Secret (SLACK_CLIENT_SECRET)").strip()
327
371
 
328
372
  console.print("[cyan]Launching browser to authorize your personal Slack account...[/cyan]")
@@ -343,16 +387,37 @@ def connect(connector_name):
343
387
  return
344
388
 
345
389
  required_vars = CONNECTOR_ENV_VARS[name]
346
- console.print(f"[cyan]Connecting '{name}' ({len(required_vars)} credential field(s) required)...[/cyan]")
390
+ console.print(f"\n[bold cyan]Connecting '{name}'[/bold cyan] ({len(required_vars)} credential field(s) required)")
391
+ console.print()
347
392
 
348
393
  credentials = {}
349
394
  for var in required_vars:
395
+ # Print the "where to get this" hint if we have one
396
+ hint = CONNECTOR_CREDENTIAL_LINKS.get(var)
397
+ if hint:
398
+ label, url = hint
399
+ if url.startswith("http"):
400
+ console.print(f" [dim]{var}[/dim]")
401
+ console.print(f" [bold]Where to get it:[/bold] {label}")
402
+ console.print(f" [cyan]{url}[/cyan]")
403
+ else:
404
+ # It's a freeform hint (e.g. "e.g. myorg/repo1")
405
+ console.print(f" [dim]{var}[/dim]")
406
+ console.print(f" [bold]Hint:[/bold] {label}")
407
+ console.print(f" [dim]{url}[/dim]")
408
+ console.print()
409
+
350
410
  existing_val = os.getenv(var, "")
351
- val = click.prompt(f"Enter {var}", default=existing_val if existing_val else None)
411
+ val = click.prompt(
412
+ f" {var}",
413
+ default=existing_val if existing_val else None,
414
+ show_default=bool(existing_val),
415
+ )
352
416
  credentials[var] = val
417
+ console.print()
353
418
 
354
419
  save_connector_credentials(name, credentials)
355
- console.print(f"[bold green][OK] Connected connector '{name}'![/bold green]")
420
+ console.print(f"[bold green][OK] '{name}' connected successfully![/bold green]")
356
421
 
357
422
 
358
423
  @cli.command()
@@ -715,8 +780,13 @@ def _provider_key_env_var(provider: str) -> str:
715
780
  return mapping.get(provider, "")
716
781
 
717
782
 
718
- def _apply_model_switch(provider_name: str, model_id: str) -> None:
719
- """Persist provider + model to .env; prompt for API key only if missing."""
783
+ def _apply_model_switch(provider_name: str, model_id: str, force_key_update: bool = False) -> None:
784
+ """Persist provider + model to .env; prompt for API key when needed.
785
+
786
+ Prompts for an API key when:
787
+ - switching to a cloud provider that has no key saved yet, OR
788
+ - force_key_update=True (explicit --update-key flag or provider change)
789
+ """
720
790
  from src.licensing.tier_mapping import get_env_file_path, save_connector_credentials
721
791
 
722
792
  env_path = get_env_file_path()
@@ -725,28 +795,45 @@ def _apply_model_switch(provider_name: str, model_id: str) -> None:
725
795
  "YCB_LLM_MODEL": model_id,
726
796
  }
727
797
 
728
- # Cloud providers: only prompt for key if not already saved
798
+ # Cloud providers: prompt for key if missing OR if user asked to update it
729
799
  if provider_name != "local":
730
800
  key_var = _provider_key_env_var(provider_name)
731
801
  existing_key = os.environ.get(key_var, "").strip()
732
- if not existing_key:
733
- provider_labels = {
734
- "openai": "OpenAI",
735
- "anthropic": "Anthropic",
736
- "openrouter": "OpenRouter",
737
- }
738
- label = provider_labels.get(provider_name, provider_name)
739
- import click as _click
740
- api_key = _click.prompt(
741
- f" Enter your {label} API key",
742
- hide_input=True,
743
- ).strip()
744
- if not api_key:
745
- console.print("[bold red]Error:[/bold red] API key cannot be empty.")
746
- return
747
- credentials[key_var] = api_key
802
+ provider_labels = {
803
+ "openai": "OpenAI",
804
+ "anthropic": "Anthropic",
805
+ "openrouter": "OpenRouter",
806
+ }
807
+ label = provider_labels.get(provider_name, provider_name)
808
+
809
+ if not existing_key or force_key_update:
810
+ if existing_key and force_key_update:
811
+ console.print(f" [dim]Updating {key_var} (press Enter to keep existing):[/dim]")
812
+ import click as _click
813
+ api_key = _click.prompt(
814
+ f" Enter your {label} API key",
815
+ hide_input=True,
816
+ default="",
817
+ show_default=False,
818
+ ).strip()
819
+ if api_key: # only overwrite if they entered something
820
+ credentials[key_var] = api_key
821
+ else:
822
+ console.print(f" [dim]Keeping existing {key_var}.[/dim]")
823
+ else:
824
+ import click as _click
825
+ api_key = _click.prompt(
826
+ f" Enter your {label} API key",
827
+ hide_input=True,
828
+ ).strip()
829
+ if not api_key:
830
+ console.print("[bold red]Error:[/bold red] API key cannot be empty.")
831
+ return
832
+ credentials[key_var] = api_key
748
833
  else:
749
- console.print(f" [dim]Using saved {_provider_key_env_var(provider_name)} (already configured)[/dim]")
834
+ # Mask the key for display: show first 8 chars + ***
835
+ masked = existing_key[:8] + "***" if len(existing_key) > 8 else "***"
836
+ console.print(f" [dim]Using saved {key_var}: {masked} (run with --update-key to change)[/dim]")
750
837
 
751
838
  save_connector_credentials("llm_provider", credentials, env_path)
752
839
 
@@ -764,20 +851,26 @@ def _apply_model_switch(provider_name: str, model_id: str) -> None:
764
851
  @click.argument("model_name", required=False, default=None)
765
852
  @click.option("--list", "list_only", is_flag=True, default=False,
766
853
  help="Print the model list without switching.")
767
- def switch_model(model_name, list_only):
854
+ @click.option("--update-key", "update_key", is_flag=True, default=False,
855
+ help="Re-prompt for the provider API key even if one is already saved.")
856
+ def switch_model(model_name, list_only, update_key):
768
857
  """Switch the active LLM model for --ask.
769
858
 
770
859
  MODEL_NAME: optional model string to switch directly (e.g. gpt-5.6-terra,
771
860
  claude-sonnet-5, gemma4:e4b). Omit to show the interactive menu.
772
861
  Any string containing '/' that is not a known named model is treated as a
773
862
  custom OpenRouter model string.
863
+
864
+ To update/change your saved API key for a provider, run:
865
+ ycb switch model --update-key
774
866
  """
775
867
  from src.qa.providers import MODELS, resolve_model_entry
776
868
 
869
+ current_provider = os.environ.get("YCB_LLM_PROVIDER", "local")
870
+
777
871
  # ── --list: just print, don't switch ────────────────────────────────
778
872
  if list_only:
779
873
  _print_model_menu()
780
- current_provider = os.environ.get("YCB_LLM_PROVIDER", "local")
781
874
  current_model = os.environ.get("YCB_LLM_MODEL", "gemma4:e4b")
782
875
  console.print(
783
876
  f"[dim]Current: [bold]{current_model}[/bold] via {current_provider}[/dim]"
@@ -789,7 +882,10 @@ def switch_model(model_name, list_only):
789
882
  # Check named models first
790
883
  entry = resolve_model_entry(model_name)
791
884
  if entry:
792
- _apply_model_switch(entry.provider, entry.model_id)
885
+ # Auto-force key update when switching to a different provider
886
+ switching_provider = entry.provider != current_provider
887
+ _apply_model_switch(entry.provider, entry.model_id,
888
+ force_key_update=update_key or switching_provider)
793
889
  return
794
890
 
795
891
  # Treat any slug with '/' as a custom OpenRouter model
@@ -797,7 +893,9 @@ def switch_model(model_name, list_only):
797
893
  console.print(
798
894
  f" [dim]Treating '{model_name}' as a custom OpenRouter model.[/dim]"
799
895
  )
800
- _apply_model_switch("openrouter", model_name)
896
+ switching_provider = "openrouter" != current_provider
897
+ _apply_model_switch("openrouter", model_name,
898
+ force_key_update=update_key or switching_provider)
801
899
  return
802
900
 
803
901
  # Unknown string and no '/'
@@ -846,10 +944,14 @@ def switch_model(model_name, list_only):
846
944
  console.print(" [yellow]Model string cannot be empty.[/yellow]")
847
945
  continue
848
946
 
849
- _apply_model_switch("openrouter", custom_model)
947
+ switching_provider = "openrouter" != current_provider
948
+ _apply_model_switch("openrouter", custom_model,
949
+ force_key_update=update_key or switching_provider)
850
950
  return
851
951
 
852
- _apply_model_switch(entry.provider, entry.model_id)
952
+ switching_provider = entry.provider != current_provider
953
+ _apply_model_switch(entry.provider, entry.model_id,
954
+ force_key_update=update_key or switching_provider)
853
955
  return
854
956
 
855
957
 
@@ -40,16 +40,6 @@ TIER_CONNECTORS: Dict[str, List[str]] = {
40
40
  "hubspot", "salesforce", "monday", "basecamp", "ashby",
41
41
  "bamboohr", "deel", "rippling",
42
42
  ],
43
- "test": [
44
- "slack", "google_docs", "github", "notion", "discord",
45
- "google_sheets", "linear", "outlook", "gitlab", "dropbox",
46
- "mixpanel", "amplitude", "algolia", "exa", "perplexity",
47
- "facebook", "gmail", "google_drive", "todoist", "web_crawler",
48
- "jira", "asana", "calendly", "clickup", "airtable",
49
- "datadog", "segment", "front", "zoom", "twitter",
50
- "hubspot", "salesforce", "monday", "basecamp", "ashby",
51
- "bamboohr", "deel", "rippling",
52
- ],
53
43
  }
54
44
 
55
45
  # Categorized grouping of all 38 connectors for clean CLI status display
@@ -104,6 +94,110 @@ CONNECTOR_ENV_VARS: Dict[str, List[str]] = {
104
94
  "rippling": ["RIPPLING_API_TOKEN"],
105
95
  }
106
96
 
97
+ # Where to get credentials — env var -> (label, url)
98
+ # Shown as a hint before each credential prompt in `ycb connect`.
99
+ CONNECTOR_CREDENTIAL_LINKS: Dict[str, Tuple[str, str]] = {
100
+ # Slack
101
+ "SLACK_CLIENT_ID": ("Slack App Dashboard", "https://api.slack.com/apps"),
102
+ "SLACK_CLIENT_SECRET": ("Slack App Dashboard", "https://api.slack.com/apps"),
103
+ "SLACK_USER_TOKEN": ("Slack App OAuth & Permissions", "https://api.slack.com/apps"),
104
+ # Google (shared across google_docs, gmail, sheets, drive)
105
+ "GOOGLE_CREDENTIALS_PATH": ("Google Cloud Console — OAuth credentials (download as credentials.json)",
106
+ "https://console.cloud.google.com/apis/credentials"),
107
+ # GitHub
108
+ "GITHUB_TOKEN": ("GitHub Personal Access Tokens", "https://github.com/settings/tokens"),
109
+ "GITHUB_REPOS": ("Your GitHub repo list", "e.g. myorg/repo1,myorg/repo2"),
110
+ # Notion
111
+ "NOTION_TOKEN": ("Notion My Integrations", "https://www.notion.so/my-integrations"),
112
+ # Discord
113
+ "DISCORD_BOT_TOKEN": ("Discord Developer Portal", "https://discord.com/developers/applications"),
114
+ "DISCORD_GUILD_IDS": ("Your Discord Server IDs", "Right-click server → Copy Server ID (enable Developer Mode first)"),
115
+ # Linear
116
+ "LINEAR_API_KEY": ("Linear API Settings", "https://linear.app/settings/api"),
117
+ # Outlook / Microsoft Graph
118
+ "MS_CLIENT_ID": ("Azure App Registrations", "https://portal.azure.com/#view/Microsoft_AAD_RegisteredApps"),
119
+ "MS_CLIENT_SECRET": ("Azure App — Certificates & Secrets", "https://portal.azure.com/#view/Microsoft_AAD_RegisteredApps"),
120
+ "MS_TENANT_ID": ("Azure Active Directory — Tenant ID", "https://portal.azure.com/#view/Microsoft_AAD_IAM/ActiveDirectoryMenuBlade"),
121
+ # GitLab
122
+ "GITLAB_TOKEN": ("GitLab Personal Access Tokens", "https://gitlab.com/-/profile/personal_access_tokens"),
123
+ "GITLAB_URL": ("Your GitLab instance URL", "e.g. https://gitlab.com or your self-hosted URL"),
124
+ "GITLAB_PROJECTS": ("Your GitLab project list", "e.g. mygroup/project1,mygroup/project2"),
125
+ # Dropbox
126
+ "DROPBOX_TOKEN": ("Dropbox App Console", "https://www.dropbox.com/developers/apps"),
127
+ # Mixpanel
128
+ "MIXPANEL_API_SECRET": ("Mixpanel Project Settings — Service Account", "https://mixpanel.com/settings/project"),
129
+ "MIXPANEL_PROJECT_ID": ("Mixpanel Project Settings", "https://mixpanel.com/settings/project"),
130
+ # Amplitude
131
+ "AMPLITUDE_API_KEY": ("Amplitude Project Settings", "https://analytics.amplitude.com/login"),
132
+ "AMPLITUDE_SECRET_KEY": ("Amplitude Project Settings", "https://analytics.amplitude.com/login"),
133
+ # Algolia
134
+ "ALGOLIA_APP_ID": ("Algolia Dashboard — API Keys", "https://dashboard.algolia.com/account/api-keys"),
135
+ "ALGOLIA_API_KEY": ("Algolia Dashboard — API Keys", "https://dashboard.algolia.com/account/api-keys"),
136
+ "ALGOLIA_INDEX_NAME": ("Your Algolia index name", "Found in Algolia Dashboard → Search → Indices"),
137
+ # Exa
138
+ "EXA_API_KEY": ("Exa Dashboard", "https://dashboard.exa.ai/api-keys"),
139
+ # Perplexity
140
+ "PERPLEXITY_API_KEY": ("Perplexity API Settings", "https://www.perplexity.ai/settings/api"),
141
+ # Facebook
142
+ "FACEBOOK_PAGE_TOKEN": ("Facebook Developer Portal — Page Access Token", "https://developers.facebook.com/apps"),
143
+ "FACEBOOK_PAGE_ID": ("Your Facebook Page ID", "Found in Facebook Page → About or Page Settings"),
144
+ # Todoist
145
+ "TODOIST_API_TOKEN": ("Todoist Integrations Settings", "https://app.todoist.com/app/settings/integrations/developer"),
146
+ # Web Crawler
147
+ "WEB_CRAWLER_START_URLS": ("Comma-separated list of URLs to crawl", "e.g. https://docs.yourcompany.com,https://yourcompany.com/blog"),
148
+ # Jira / Atlassian
149
+ "JIRA_DOMAIN": ("Your Jira workspace domain", "e.g. yourcompany.atlassian.net"),
150
+ "JIRA_EMAIL": ("Your Atlassian account email", "The email you use to log in to Jira"),
151
+ "JIRA_API_TOKEN": ("Atlassian API Tokens", "https://id.atlassian.com/manage-profile/security/api-tokens"),
152
+ # Asana
153
+ "ASANA_TOKEN": ("Asana Developer Console — Personal Access Token", "https://app.asana.com/0/developer-console"),
154
+ "ASANA_WORKSPACE_GID": ("Your Asana Workspace GID", "Found via GET https://app.asana.com/api/1.0/workspaces"),
155
+ # Calendly
156
+ "CALENDLY_TOKEN": ("Calendly Developer Portal — Personal Access Token", "https://developer.calendly.com/how-to-authenticate-with-personal-access-tokens"),
157
+ "CALENDLY_USER_URI": ("Your Calendly User URI", "Found via GET https://api.calendly.com/users/me"),
158
+ # ClickUp
159
+ "CLICKUP_API_TOKEN": ("ClickUp Settings — Apps", "https://app.clickup.com/settings/apps"),
160
+ "CLICKUP_TEAM_ID": ("Your ClickUp Team ID", "Found in ClickUp URL: app.clickup.com/<team_id>/"),
161
+ # Airtable
162
+ "AIRTABLE_TOKEN": ("Airtable Developer Hub — Personal Access Tokens", "https://airtable.com/create/tokens"),
163
+ "AIRTABLE_BASE_IDS": ("Your Airtable Base IDs", "Found in Airtable API docs page for each base: airtable.com/appXXXXXX/api/docs"),
164
+ # Datadog
165
+ "DATADOG_API_KEY": ("Datadog Organization Settings — API Keys", "https://app.datadoghq.com/organization-settings/api-keys"),
166
+ "DATADOG_APP_KEY": ("Datadog Organization Settings — Application Keys", "https://app.datadoghq.com/organization-settings/application-keys"),
167
+ # Segment
168
+ "SEGMENT_PERSONAS_TOKEN": ("Segment App — Access Tokens", "https://app.segment.com/goto-my-workspace/settings/access-management"),
169
+ "SEGMENT_SPACE_ID": ("Your Segment Space ID", "Found in Segment → Engage → Settings → API Access"),
170
+ # Front
171
+ "FRONT_API_TOKEN": ("Front App Settings — API Tokens", "https://app.frontapp.com/settings/tools/api"),
172
+ # Zoom
173
+ "ZOOM_ACCOUNT_ID": ("Zoom Marketplace — Server-to-Server OAuth App", "https://marketplace.zoom.us/develop/create"),
174
+ "ZOOM_CLIENT_ID": ("Zoom Marketplace — Server-to-Server OAuth App", "https://marketplace.zoom.us/develop/create"),
175
+ "ZOOM_CLIENT_SECRET": ("Zoom Marketplace — Server-to-Server OAuth App", "https://marketplace.zoom.us/develop/create"),
176
+ # Twitter / X
177
+ "TWITTER_BEARER_TOKEN": ("Twitter Developer Portal — Project/App Keys", "https://developer.twitter.com/en/portal/dashboard"),
178
+ "TWITTER_SEARCH_QUERIES": ("Comma-separated search queries", "e.g. @yourcompany,#yourproduct"),
179
+ # HubSpot
180
+ "HUBSPOT_ACCESS_TOKEN": ("HubSpot Private App Access Token", "https://app.hubspot.com/private-apps"),
181
+ # Salesforce
182
+ "SALESFORCE_CLIENT_ID": ("Salesforce Connected App Consumer Key", "Setup → Apps → App Manager → New Connected App"),
183
+ "SALESFORCE_CLIENT_SECRET": ("Salesforce Connected App Consumer Secret", "Setup → Apps → App Manager → New Connected App"),
184
+ "SALESFORCE_DOMAIN": ("Your Salesforce domain", "e.g. yourcompany.my.salesforce.com"),
185
+ # Monday
186
+ "MONDAY_API_TOKEN": ("Monday.com Developer Center — API Token", "https://auth.monday.com/users/sign_in (Profile → Developers → API)"),
187
+ # Basecamp
188
+ "BASECAMP_ACCESS_TOKEN": ("Basecamp — Launchpad OAuth / Personal Tokens", "https://launchpad.37signals.com/"),
189
+ "BASECAMP_ACCOUNT_ID": ("Your Basecamp Account ID", "Found in your Basecamp URL: 3.basecamp.com/<account_id>/"),
190
+ # Ashby
191
+ "ASHBY_API_KEY": ("Ashby Settings — API Keys", "https://app.ashbyhq.com/admin/api/keys"),
192
+ # BambooHR
193
+ "BAMBOOHR_API_KEY": ("BambooHR API Access — Generate API Key", "https://app.bamboohr.com/settings/permissions/"),
194
+ "BAMBOOHR_SUBDOMAIN": ("Your BambooHR subdomain", "e.g. if your URL is mycompany.bamboohr.com, enter 'mycompany'"),
195
+ # Deel
196
+ "DEEL_API_TOKEN": ("Deel API Settings", "https://app.deel.com/settings/api"),
197
+ # Rippling
198
+ "RIPPLING_API_TOKEN": ("Rippling API Credentials", "https://app.rippling.com/api"),
199
+ }
200
+
107
201
 
108
202
  def get_cached_plan_tier(cache_dir: Optional[str] = None, expected_key: Optional[str] = None) -> str:
109
203
  """Read the plan_tier from ~/.company-brain/license.json. Defaults to 'solo' if unconfigured.
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes