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.
- {ycb-3.4.2 → ycb-3.4.3}/PKG-INFO +7 -4
- {ycb-3.4.2 → ycb-3.4.3}/README.md +6 -3
- {ycb-3.4.2 → ycb-3.4.3}/pyproject.toml +1 -1
- {ycb-3.4.2 → ycb-3.4.3}/src/__init__.py +1 -1
- {ycb-3.4.2 → ycb-3.4.3}/src/cli/main.py +133 -31
- {ycb-3.4.2 → ycb-3.4.3}/src/licensing/tier_mapping.py +104 -10
- {ycb-3.4.2 → ycb-3.4.3}/.env.example +0 -0
- {ycb-3.4.2 → ycb-3.4.3}/.gitignore +0 -0
- {ycb-3.4.2 → ycb-3.4.3}/src/cli/__init__.py +0 -0
- {ycb-3.4.2 → ycb-3.4.3}/src/cli/ui/__init__.py +0 -0
- {ycb-3.4.2 → ycb-3.4.3}/src/cli/ui/animation.py +0 -0
- {ycb-3.4.2 → ycb-3.4.3}/src/cli/ui/components.py +0 -0
- {ycb-3.4.2 → ycb-3.4.3}/src/cli/ui/dashboard.py +0 -0
- {ycb-3.4.2 → ycb-3.4.3}/src/cli/ui/logger.py +0 -0
- {ycb-3.4.2 → ycb-3.4.3}/src/config/__init__.py +0 -0
- {ycb-3.4.2 → ycb-3.4.3}/src/connectors/__init__.py +0 -0
- {ycb-3.4.2 → ycb-3.4.3}/src/connectors/airtable_connector.py +0 -0
- {ycb-3.4.2 → ycb-3.4.3}/src/connectors/analytics_connector.py +0 -0
- {ycb-3.4.2 → ycb-3.4.3}/src/connectors/asana_connector.py +0 -0
- {ycb-3.4.2 → ycb-3.4.3}/src/connectors/ashby_connector.py +0 -0
- {ycb-3.4.2 → ycb-3.4.3}/src/connectors/bamboohr_connector.py +0 -0
- {ycb-3.4.2 → ycb-3.4.3}/src/connectors/base_connector.py +0 -0
- {ycb-3.4.2 → ycb-3.4.3}/src/connectors/basecamp_connector.py +0 -0
- {ycb-3.4.2 → ycb-3.4.3}/src/connectors/calendly_connector.py +0 -0
- {ycb-3.4.2 → ycb-3.4.3}/src/connectors/clickup_connector.py +0 -0
- {ycb-3.4.2 → ycb-3.4.3}/src/connectors/datadog_connector.py +0 -0
- {ycb-3.4.2 → ycb-3.4.3}/src/connectors/deel_connector.py +0 -0
- {ycb-3.4.2 → ycb-3.4.3}/src/connectors/discord_connector.py +0 -0
- {ycb-3.4.2 → ycb-3.4.3}/src/connectors/dropbox_connector.py +0 -0
- {ycb-3.4.2 → ycb-3.4.3}/src/connectors/facebook_connector.py +0 -0
- {ycb-3.4.2 → ycb-3.4.3}/src/connectors/front_connector.py +0 -0
- {ycb-3.4.2 → ycb-3.4.3}/src/connectors/github_connector.py +0 -0
- {ycb-3.4.2 → ycb-3.4.3}/src/connectors/gitlab_connector.py +0 -0
- {ycb-3.4.2 → ycb-3.4.3}/src/connectors/gmail_connector.py +0 -0
- {ycb-3.4.2 → ycb-3.4.3}/src/connectors/google_docs_connector.py +0 -0
- {ycb-3.4.2 → ycb-3.4.3}/src/connectors/google_drive_connector.py +0 -0
- {ycb-3.4.2 → ycb-3.4.3}/src/connectors/google_sheets_connector.py +0 -0
- {ycb-3.4.2 → ycb-3.4.3}/src/connectors/hubspot_connector.py +0 -0
- {ycb-3.4.2 → ycb-3.4.3}/src/connectors/jira_connector.py +0 -0
- {ycb-3.4.2 → ycb-3.4.3}/src/connectors/linear_connector.py +0 -0
- {ycb-3.4.2 → ycb-3.4.3}/src/connectors/monday_connector.py +0 -0
- {ycb-3.4.2 → ycb-3.4.3}/src/connectors/notion_connector.py +0 -0
- {ycb-3.4.2 → ycb-3.4.3}/src/connectors/outlook_teams_connector.py +0 -0
- {ycb-3.4.2 → ycb-3.4.3}/src/connectors/registry.py +0 -0
- {ycb-3.4.2 → ycb-3.4.3}/src/connectors/rippling_connector.py +0 -0
- {ycb-3.4.2 → ycb-3.4.3}/src/connectors/salesforce_connector.py +0 -0
- {ycb-3.4.2 → ycb-3.4.3}/src/connectors/search_connector.py +0 -0
- {ycb-3.4.2 → ycb-3.4.3}/src/connectors/segment_connector.py +0 -0
- {ycb-3.4.2 → ycb-3.4.3}/src/connectors/slack_connector.py +0 -0
- {ycb-3.4.2 → ycb-3.4.3}/src/connectors/slack_oauth.py +0 -0
- {ycb-3.4.2 → ycb-3.4.3}/src/connectors/todoist_connector.py +0 -0
- {ycb-3.4.2 → ycb-3.4.3}/src/connectors/twitter_connector.py +0 -0
- {ycb-3.4.2 → ycb-3.4.3}/src/connectors/web_crawler_connector.py +0 -0
- {ycb-3.4.2 → ycb-3.4.3}/src/connectors/zoom_connector.py +0 -0
- {ycb-3.4.2 → ycb-3.4.3}/src/licensing/__init__.py +0 -0
- {ycb-3.4.2 → ycb-3.4.3}/src/licensing/validator.py +0 -0
- {ycb-3.4.2 → ycb-3.4.3}/src/models/__init__.py +0 -0
- {ycb-3.4.2 → ycb-3.4.3}/src/models/domain.py +0 -0
- {ycb-3.4.2 → ycb-3.4.3}/src/processors/__init__.py +0 -0
- {ycb-3.4.2 → ycb-3.4.3}/src/processors/chunker.py +0 -0
- {ycb-3.4.2 → ycb-3.4.3}/src/processors/knowledge_extractor.py +0 -0
- {ycb-3.4.2 → ycb-3.4.3}/src/processors/skills_synthesizer.py +0 -0
- {ycb-3.4.2 → ycb-3.4.3}/src/qa/__init__.py +0 -0
- {ycb-3.4.2 → ycb-3.4.3}/src/qa/ask_engine.py +0 -0
- {ycb-3.4.2 → ycb-3.4.3}/src/qa/providers/__init__.py +0 -0
- {ycb-3.4.2 → ycb-3.4.3}/src/qa/providers/anthropic_provider.py +0 -0
- {ycb-3.4.2 → ycb-3.4.3}/src/qa/providers/base.py +0 -0
- {ycb-3.4.2 → ycb-3.4.3}/src/qa/providers/ollama_provider.py +0 -0
- {ycb-3.4.2 → ycb-3.4.3}/src/qa/providers/openai_provider.py +0 -0
- {ycb-3.4.2 → ycb-3.4.3}/src/qa/providers/openrouter_provider.py +0 -0
- {ycb-3.4.2 → ycb-3.4.3}/src/storage/__init__.py +0 -0
- {ycb-3.4.2 → ycb-3.4.3}/src/storage/database.py +0 -0
- {ycb-3.4.2 → ycb-3.4.3}/src/storage/storage_manager.py +0 -0
- {ycb-3.4.2 → ycb-3.4.3}/src/sync/__init__.py +0 -0
- {ycb-3.4.2 → ycb-3.4.3}/src/sync/sync_scheduler.py +0 -0
{ycb-3.4.2 → ycb-3.4.3}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: ycb
|
|
3
|
-
Version: 3.4.
|
|
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.
|
|
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.
|
|
29
|
-
> - **
|
|
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.
|
|
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.
|
|
8
|
-
> - **
|
|
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.
|
|
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
|
+
__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]
|
|
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]
|
|
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)
|
|
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(
|
|
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]
|
|
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
|
|
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:
|
|
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
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
f"
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|