ycb 3.4.2__tar.gz → 3.4.4__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.4}/.env.example +1 -1
- {ycb-3.4.2 → ycb-3.4.4}/PKG-INFO +8 -4
- {ycb-3.4.2 → ycb-3.4.4}/README.md +7 -3
- {ycb-3.4.2 → ycb-3.4.4}/pyproject.toml +1 -1
- {ycb-3.4.2 → ycb-3.4.4}/src/__init__.py +1 -1
- {ycb-3.4.2 → ycb-3.4.4}/src/cli/main.py +138 -34
- {ycb-3.4.2 → ycb-3.4.4}/src/connectors/airtable_connector.py +1 -1
- {ycb-3.4.2 → ycb-3.4.4}/src/connectors/analytics_connector.py +2 -2
- {ycb-3.4.2 → ycb-3.4.4}/src/connectors/asana_connector.py +1 -1
- {ycb-3.4.2 → ycb-3.4.4}/src/connectors/ashby_connector.py +1 -1
- {ycb-3.4.2 → ycb-3.4.4}/src/connectors/bamboohr_connector.py +1 -1
- {ycb-3.4.2 → ycb-3.4.4}/src/connectors/base_connector.py +79 -0
- {ycb-3.4.2 → ycb-3.4.4}/src/connectors/basecamp_connector.py +1 -1
- {ycb-3.4.2 → ycb-3.4.4}/src/connectors/calendly_connector.py +1 -1
- {ycb-3.4.2 → ycb-3.4.4}/src/connectors/clickup_connector.py +1 -1
- {ycb-3.4.2 → ycb-3.4.4}/src/connectors/datadog_connector.py +1 -1
- {ycb-3.4.2 → ycb-3.4.4}/src/connectors/deel_connector.py +1 -1
- {ycb-3.4.2 → ycb-3.4.4}/src/connectors/discord_connector.py +1 -1
- {ycb-3.4.2 → ycb-3.4.4}/src/connectors/dropbox_connector.py +1 -1
- {ycb-3.4.2 → ycb-3.4.4}/src/connectors/facebook_connector.py +1 -1
- {ycb-3.4.2 → ycb-3.4.4}/src/connectors/front_connector.py +1 -1
- {ycb-3.4.2 → ycb-3.4.4}/src/connectors/github_connector.py +1 -1
- {ycb-3.4.2 → ycb-3.4.4}/src/connectors/gitlab_connector.py +1 -1
- {ycb-3.4.2 → ycb-3.4.4}/src/connectors/hubspot_connector.py +1 -1
- {ycb-3.4.2 → ycb-3.4.4}/src/connectors/jira_connector.py +1 -1
- {ycb-3.4.2 → ycb-3.4.4}/src/connectors/linear_connector.py +1 -1
- {ycb-3.4.2 → ycb-3.4.4}/src/connectors/monday_connector.py +1 -1
- {ycb-3.4.2 → ycb-3.4.4}/src/connectors/notion_connector.py +1 -1
- {ycb-3.4.2 → ycb-3.4.4}/src/connectors/outlook_teams_connector.py +2 -2
- {ycb-3.4.2 → ycb-3.4.4}/src/connectors/rippling_connector.py +1 -1
- {ycb-3.4.2 → ycb-3.4.4}/src/connectors/salesforce_connector.py +1 -1
- {ycb-3.4.2 → ycb-3.4.4}/src/connectors/search_connector.py +3 -3
- {ycb-3.4.2 → ycb-3.4.4}/src/connectors/segment_connector.py +1 -1
- {ycb-3.4.2 → ycb-3.4.4}/src/connectors/slack_connector.py +6 -2
- {ycb-3.4.2 → ycb-3.4.4}/src/connectors/todoist_connector.py +1 -1
- {ycb-3.4.2 → ycb-3.4.4}/src/connectors/twitter_connector.py +1 -1
- {ycb-3.4.2 → ycb-3.4.4}/src/connectors/zoom_connector.py +1 -1
- {ycb-3.4.2 → ycb-3.4.4}/src/licensing/tier_mapping.py +113 -14
- {ycb-3.4.2 → ycb-3.4.4}/.gitignore +0 -0
- {ycb-3.4.2 → ycb-3.4.4}/src/cli/__init__.py +0 -0
- {ycb-3.4.2 → ycb-3.4.4}/src/cli/ui/__init__.py +0 -0
- {ycb-3.4.2 → ycb-3.4.4}/src/cli/ui/animation.py +0 -0
- {ycb-3.4.2 → ycb-3.4.4}/src/cli/ui/components.py +0 -0
- {ycb-3.4.2 → ycb-3.4.4}/src/cli/ui/dashboard.py +0 -0
- {ycb-3.4.2 → ycb-3.4.4}/src/cli/ui/logger.py +0 -0
- {ycb-3.4.2 → ycb-3.4.4}/src/config/__init__.py +0 -0
- {ycb-3.4.2 → ycb-3.4.4}/src/connectors/__init__.py +0 -0
- {ycb-3.4.2 → ycb-3.4.4}/src/connectors/gmail_connector.py +0 -0
- {ycb-3.4.2 → ycb-3.4.4}/src/connectors/google_docs_connector.py +0 -0
- {ycb-3.4.2 → ycb-3.4.4}/src/connectors/google_drive_connector.py +0 -0
- {ycb-3.4.2 → ycb-3.4.4}/src/connectors/google_sheets_connector.py +0 -0
- {ycb-3.4.2 → ycb-3.4.4}/src/connectors/registry.py +0 -0
- {ycb-3.4.2 → ycb-3.4.4}/src/connectors/slack_oauth.py +0 -0
- {ycb-3.4.2 → ycb-3.4.4}/src/connectors/web_crawler_connector.py +0 -0
- {ycb-3.4.2 → ycb-3.4.4}/src/licensing/__init__.py +0 -0
- {ycb-3.4.2 → ycb-3.4.4}/src/licensing/validator.py +0 -0
- {ycb-3.4.2 → ycb-3.4.4}/src/models/__init__.py +0 -0
- {ycb-3.4.2 → ycb-3.4.4}/src/models/domain.py +0 -0
- {ycb-3.4.2 → ycb-3.4.4}/src/processors/__init__.py +0 -0
- {ycb-3.4.2 → ycb-3.4.4}/src/processors/chunker.py +0 -0
- {ycb-3.4.2 → ycb-3.4.4}/src/processors/knowledge_extractor.py +0 -0
- {ycb-3.4.2 → ycb-3.4.4}/src/processors/skills_synthesizer.py +0 -0
- {ycb-3.4.2 → ycb-3.4.4}/src/qa/__init__.py +0 -0
- {ycb-3.4.2 → ycb-3.4.4}/src/qa/ask_engine.py +0 -0
- {ycb-3.4.2 → ycb-3.4.4}/src/qa/providers/__init__.py +0 -0
- {ycb-3.4.2 → ycb-3.4.4}/src/qa/providers/anthropic_provider.py +0 -0
- {ycb-3.4.2 → ycb-3.4.4}/src/qa/providers/base.py +0 -0
- {ycb-3.4.2 → ycb-3.4.4}/src/qa/providers/ollama_provider.py +0 -0
- {ycb-3.4.2 → ycb-3.4.4}/src/qa/providers/openai_provider.py +0 -0
- {ycb-3.4.2 → ycb-3.4.4}/src/qa/providers/openrouter_provider.py +0 -0
- {ycb-3.4.2 → ycb-3.4.4}/src/storage/__init__.py +0 -0
- {ycb-3.4.2 → ycb-3.4.4}/src/storage/database.py +0 -0
- {ycb-3.4.2 → ycb-3.4.4}/src/storage/storage_manager.py +0 -0
- {ycb-3.4.2 → ycb-3.4.4}/src/sync/__init__.py +0 -0
- {ycb-3.4.2 → ycb-3.4.4}/src/sync/sync_scheduler.py +0 -0
|
@@ -36,7 +36,7 @@ SLACK_CLIENT_ID=your_slack_app_client_id
|
|
|
36
36
|
SLACK_CLIENT_SECRET=your_slack_app_client_secret
|
|
37
37
|
|
|
38
38
|
# User Configuration: Generated automatically per user when running 'ycb connect slack'
|
|
39
|
-
SLACK_USER_TOKEN=xoxp-your-user-token
|
|
39
|
+
# SLACK_USER_TOKEN=xoxp-your-user-token
|
|
40
40
|
|
|
41
41
|
# DEPRECATED (v3.1.0+): Bot Tokens (xoxb-) are no longer supported for privacy/RBAC
|
|
42
42
|
# SLACK_BOT_TOKEN=xoxb-your-token
|
{ycb-3.4.2 → ycb-3.4.4}/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.4
|
|
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,18 @@ 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.4)
|
|
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.4 — Test Tier Enterprise Access & Placeholder Credential Filtering**:
|
|
29
|
+
> - **Test License Key Support**: Internal/test license keys (`tier: "test"`) now correctly map to full Enterprise-level connector access (all 38 connectors unlocked) across `status`, `sync`, and `connect`.
|
|
30
|
+
> - **Template Placeholder Filtering**: Connectors no longer falsely report as `[ACTIVE]` when `.env.example` placeholders (e.g. `xoxp-your-user-token`, `your_...`) are present in local configuration files.
|
|
31
|
+
> - **Guided `ycb connect` with Credential Links**: Every credential prompt 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.
|
|
32
|
+
> - **Smart `ycb sync` Onboarding**: When no connectors are configured, `ycb sync` shows a clean panel with tier-specific `ycb connect` examples instead of launching an empty idle dashboard.
|
|
33
|
+
> - **Max Monthly Subscription Tier**: `"max"` tier unlocks all **38 connectors** with automated 30-day subscription management and background self-healing.
|
|
30
34
|
> - **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
35
|
> - **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
36
|
> - **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,15 @@
|
|
|
1
|
-
# 🧠 Your Company Brain (v3.4.
|
|
1
|
+
# 🧠 Your Company Brain (v3.4.4)
|
|
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.4 — Test Tier Enterprise Access & Placeholder Credential Filtering**:
|
|
8
|
+
> - **Test License Key Support**: Internal/test license keys (`tier: "test"`) now correctly map to full Enterprise-level connector access (all 38 connectors unlocked) across `status`, `sync`, and `connect`.
|
|
9
|
+
> - **Template Placeholder Filtering**: Connectors no longer falsely report as `[ACTIVE]` when `.env.example` placeholders (e.g. `xoxp-your-user-token`, `your_...`) are present in local configuration files.
|
|
10
|
+
> - **Guided `ycb connect` with Credential Links**: Every credential prompt 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.
|
|
11
|
+
> - **Smart `ycb sync` Onboarding**: When no connectors are configured, `ycb sync` shows a clean panel with tier-specific `ycb connect` examples instead of launching an empty idle dashboard.
|
|
12
|
+
> - **Max Monthly Subscription Tier**: `"max"` tier unlocks all **38 connectors** with automated 30-day subscription management and background self-healing.
|
|
9
13
|
> - **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
14
|
> - **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
15
|
> - **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.4"
|
|
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.4"
|
|
@@ -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,
|
|
@@ -183,8 +184,9 @@ def login(license_key, email):
|
|
|
183
184
|
os.environ["YCB_USER_EMAIL"] = email
|
|
184
185
|
|
|
185
186
|
plan_tier = res.plan_tier or "solo"
|
|
187
|
+
tier_title = "Test (Enterprise Access)" if plan_tier.lower() == "test" else plan_tier.title()
|
|
186
188
|
console.print(f"\n[bold green][OK] Successfully logged in as [cyan]{email}[/cyan]![/bold green]")
|
|
187
|
-
console.print(f"Plan tier: [bold cyan]{
|
|
189
|
+
console.print(f"Plan tier: [bold cyan]{tier_title}[/bold cyan] | Seats: [bold cyan]{res.seat_limit or 1}[/bold cyan]\n")
|
|
188
190
|
|
|
189
191
|
|
|
190
192
|
@cli.command()
|
|
@@ -240,6 +242,39 @@ def sync(days, once, interactive, plain, connector):
|
|
|
240
242
|
else:
|
|
241
243
|
target_connectors = None
|
|
242
244
|
|
|
245
|
+
# ── Pre-flight connector check ────────────────────────────────────────────
|
|
246
|
+
# Resolve enabled connectors once here so we can give immediate feedback.
|
|
247
|
+
# We re-use the same list for the scheduler below.
|
|
248
|
+
enabled_connectors = get_enabled_connectors(target_connectors=target_connectors, plan_tier=plan_tier)
|
|
249
|
+
|
|
250
|
+
if not enabled_connectors:
|
|
251
|
+
# Suggest the first few connectors allowed on the user's tier
|
|
252
|
+
tier_connectors = TIER_CONNECTORS.get(plan_tier.lower(), ["google_docs", "slack"])
|
|
253
|
+
examples = tier_connectors[:3] if tier_connectors else ["google_docs", "slack", "github"]
|
|
254
|
+
example_lines = "\n".join(f" [cyan]ycb connect {c}[/cyan]" for c in examples)
|
|
255
|
+
|
|
256
|
+
console.print(
|
|
257
|
+
Panel.fit(
|
|
258
|
+
"[bold yellow]No connectors are currently connected or configured.[/bold yellow]\n\n"
|
|
259
|
+
"To ingest and sync your company knowledge, connect at least one platform:\n"
|
|
260
|
+
" [bold cyan]ycb connect <connector_name>[/bold cyan]\n\n"
|
|
261
|
+
f"[bold]Available in your {plan_tier.title()} plan:[/bold]\n"
|
|
262
|
+
f"{example_lines}\n\n"
|
|
263
|
+
"Run [bold cyan]ycb status[/bold cyan] to see all available connectors for your plan tier.",
|
|
264
|
+
title="[bold cyan]Company Brain — Sync[/bold cyan]",
|
|
265
|
+
border_style="yellow",
|
|
266
|
+
)
|
|
267
|
+
)
|
|
268
|
+
return
|
|
269
|
+
|
|
270
|
+
# Show a brief summary of what will be synced
|
|
271
|
+
connector_names = [c.get_source_name() for c in enabled_connectors]
|
|
272
|
+
console.print(
|
|
273
|
+
f"[bold green][OK][/bold green] Connected applications "
|
|
274
|
+
f"({len(connector_names)}): [bold cyan]{', '.join(connector_names)}[/bold cyan]\n"
|
|
275
|
+
)
|
|
276
|
+
# ─────────────────────────────────────────────────────────────────────────
|
|
277
|
+
|
|
243
278
|
scheduler = SyncScheduler(sync_interval_minutes=30)
|
|
244
279
|
|
|
245
280
|
# Setup UI state and logging
|
|
@@ -314,15 +349,25 @@ def connect(connector_name):
|
|
|
314
349
|
return
|
|
315
350
|
|
|
316
351
|
if name == "slack":
|
|
352
|
+
console.print()
|
|
353
|
+
console.print(
|
|
354
|
+
"[bold]Slack requires a Slack App with OAuth enabled.[/bold]\n"
|
|
355
|
+
" [dim]Where to get credentials:[/dim] "
|
|
356
|
+
"[cyan]https://api.slack.com/apps[/cyan]\n"
|
|
357
|
+
" 1. Create a new Slack App (or use an existing one)\n"
|
|
358
|
+
" 2. Under [bold]OAuth & Permissions[/bold], add the user token scopes you need\n"
|
|
359
|
+
" 3. Copy the [bold]Client ID[/bold] and [bold]Client Secret[/bold] from [bold]Basic Information[/bold]"
|
|
360
|
+
)
|
|
361
|
+
console.print()
|
|
317
362
|
client_id = os.getenv("SLACK_CLIENT_ID", "")
|
|
318
363
|
client_secret = os.getenv("SLACK_CLIENT_SECRET", "")
|
|
319
364
|
|
|
320
365
|
if not client_id:
|
|
321
|
-
console.print("[cyan]
|
|
366
|
+
console.print("[cyan]SLACK_CLIENT_ID not found in config.[/cyan]")
|
|
322
367
|
client_id = click.prompt("Enter Slack App Client ID (SLACK_CLIENT_ID)").strip()
|
|
323
368
|
|
|
324
369
|
if not client_secret:
|
|
325
|
-
console.print("[cyan]
|
|
370
|
+
console.print("[cyan]SLACK_CLIENT_SECRET not found in config.[/cyan]")
|
|
326
371
|
client_secret = click.prompt("Enter Slack App Client Secret (SLACK_CLIENT_SECRET)").strip()
|
|
327
372
|
|
|
328
373
|
console.print("[cyan]Launching browser to authorize your personal Slack account...[/cyan]")
|
|
@@ -343,16 +388,37 @@ def connect(connector_name):
|
|
|
343
388
|
return
|
|
344
389
|
|
|
345
390
|
required_vars = CONNECTOR_ENV_VARS[name]
|
|
346
|
-
console.print(f"[cyan]Connecting '{name}' ({len(required_vars)} credential field(s) required)
|
|
391
|
+
console.print(f"\n[bold cyan]Connecting '{name}'[/bold cyan] ({len(required_vars)} credential field(s) required)")
|
|
392
|
+
console.print()
|
|
347
393
|
|
|
348
394
|
credentials = {}
|
|
349
395
|
for var in required_vars:
|
|
396
|
+
# Print the "where to get this" hint if we have one
|
|
397
|
+
hint = CONNECTOR_CREDENTIAL_LINKS.get(var)
|
|
398
|
+
if hint:
|
|
399
|
+
label, url = hint
|
|
400
|
+
if url.startswith("http"):
|
|
401
|
+
console.print(f" [dim]{var}[/dim]")
|
|
402
|
+
console.print(f" [bold]Where to get it:[/bold] {label}")
|
|
403
|
+
console.print(f" [cyan]{url}[/cyan]")
|
|
404
|
+
else:
|
|
405
|
+
# It's a freeform hint (e.g. "e.g. myorg/repo1")
|
|
406
|
+
console.print(f" [dim]{var}[/dim]")
|
|
407
|
+
console.print(f" [bold]Hint:[/bold] {label}")
|
|
408
|
+
console.print(f" [dim]{url}[/dim]")
|
|
409
|
+
console.print()
|
|
410
|
+
|
|
350
411
|
existing_val = os.getenv(var, "")
|
|
351
|
-
val = click.prompt(
|
|
412
|
+
val = click.prompt(
|
|
413
|
+
f" {var}",
|
|
414
|
+
default=existing_val if existing_val else None,
|
|
415
|
+
show_default=bool(existing_val),
|
|
416
|
+
)
|
|
352
417
|
credentials[var] = val
|
|
418
|
+
console.print()
|
|
353
419
|
|
|
354
420
|
save_connector_credentials(name, credentials)
|
|
355
|
-
console.print(f"[bold green][OK]
|
|
421
|
+
console.print(f"[bold green][OK] '{name}' connected successfully![/bold green]")
|
|
356
422
|
|
|
357
423
|
|
|
358
424
|
@cli.command()
|
|
@@ -495,7 +561,8 @@ def status():
|
|
|
495
561
|
|
|
496
562
|
account_label = f" | [bold cyan]Account:[/bold cyan] [bold white]{active_email}[/bold white]" if active_email else ""
|
|
497
563
|
key_label = f" | [bold cyan]Key:[/bold cyan] [dim]{active_key}[/dim]" if active_key else ""
|
|
498
|
-
|
|
564
|
+
tier_title = "Test (Enterprise Access)" if plan_tier.lower() == "test" else plan_tier.title()
|
|
565
|
+
console.print(f"\n[bold cyan]License Tier:[/bold cyan] [bold yellow]{tier_title}[/bold yellow]{account_label}{key_label}\n")
|
|
499
566
|
|
|
500
567
|
def print_grouped(connectors_list, item_format_fn):
|
|
501
568
|
conn_set = set(connectors_list)
|
|
@@ -526,7 +593,7 @@ def status():
|
|
|
526
593
|
if locked_connectors:
|
|
527
594
|
print_grouped(locked_connectors, lambda c: f" [bold red][LOCKED][/bold red] [red]{c}[/red] [gray50](Requires Enterprise tier)[/gray50]")
|
|
528
595
|
else:
|
|
529
|
-
console.print(" [gray50](None - Max/Enterprise unlocks all connectors!)[/gray50]")
|
|
596
|
+
console.print(" [gray50](None - Max/Enterprise/Test unlocks all connectors!)[/gray50]")
|
|
530
597
|
|
|
531
598
|
# Also display skills summary table if available
|
|
532
599
|
skills = StorageManager.get_all_skills()
|
|
@@ -715,8 +782,13 @@ def _provider_key_env_var(provider: str) -> str:
|
|
|
715
782
|
return mapping.get(provider, "")
|
|
716
783
|
|
|
717
784
|
|
|
718
|
-
def _apply_model_switch(provider_name: str, model_id: str) -> None:
|
|
719
|
-
"""Persist provider + model to .env; prompt for API key
|
|
785
|
+
def _apply_model_switch(provider_name: str, model_id: str, force_key_update: bool = False) -> None:
|
|
786
|
+
"""Persist provider + model to .env; prompt for API key when needed.
|
|
787
|
+
|
|
788
|
+
Prompts for an API key when:
|
|
789
|
+
- switching to a cloud provider that has no key saved yet, OR
|
|
790
|
+
- force_key_update=True (explicit --update-key flag or provider change)
|
|
791
|
+
"""
|
|
720
792
|
from src.licensing.tier_mapping import get_env_file_path, save_connector_credentials
|
|
721
793
|
|
|
722
794
|
env_path = get_env_file_path()
|
|
@@ -725,28 +797,45 @@ def _apply_model_switch(provider_name: str, model_id: str) -> None:
|
|
|
725
797
|
"YCB_LLM_MODEL": model_id,
|
|
726
798
|
}
|
|
727
799
|
|
|
728
|
-
# Cloud providers:
|
|
800
|
+
# Cloud providers: prompt for key if missing OR if user asked to update it
|
|
729
801
|
if provider_name != "local":
|
|
730
802
|
key_var = _provider_key_env_var(provider_name)
|
|
731
803
|
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
|
-
|
|
804
|
+
provider_labels = {
|
|
805
|
+
"openai": "OpenAI",
|
|
806
|
+
"anthropic": "Anthropic",
|
|
807
|
+
"openrouter": "OpenRouter",
|
|
808
|
+
}
|
|
809
|
+
label = provider_labels.get(provider_name, provider_name)
|
|
810
|
+
|
|
811
|
+
if not existing_key or force_key_update:
|
|
812
|
+
if existing_key and force_key_update:
|
|
813
|
+
console.print(f" [dim]Updating {key_var} (press Enter to keep existing):[/dim]")
|
|
814
|
+
import click as _click
|
|
815
|
+
api_key = _click.prompt(
|
|
816
|
+
f" Enter your {label} API key",
|
|
817
|
+
hide_input=True,
|
|
818
|
+
default="",
|
|
819
|
+
show_default=False,
|
|
820
|
+
).strip()
|
|
821
|
+
if api_key: # only overwrite if they entered something
|
|
822
|
+
credentials[key_var] = api_key
|
|
823
|
+
else:
|
|
824
|
+
console.print(f" [dim]Keeping existing {key_var}.[/dim]")
|
|
825
|
+
else:
|
|
826
|
+
import click as _click
|
|
827
|
+
api_key = _click.prompt(
|
|
828
|
+
f" Enter your {label} API key",
|
|
829
|
+
hide_input=True,
|
|
830
|
+
).strip()
|
|
831
|
+
if not api_key:
|
|
832
|
+
console.print("[bold red]Error:[/bold red] API key cannot be empty.")
|
|
833
|
+
return
|
|
834
|
+
credentials[key_var] = api_key
|
|
748
835
|
else:
|
|
749
|
-
|
|
836
|
+
# Mask the key for display: show first 8 chars + ***
|
|
837
|
+
masked = existing_key[:8] + "***" if len(existing_key) > 8 else "***"
|
|
838
|
+
console.print(f" [dim]Using saved {key_var}: {masked} (run with --update-key to change)[/dim]")
|
|
750
839
|
|
|
751
840
|
save_connector_credentials("llm_provider", credentials, env_path)
|
|
752
841
|
|
|
@@ -764,20 +853,26 @@ def _apply_model_switch(provider_name: str, model_id: str) -> None:
|
|
|
764
853
|
@click.argument("model_name", required=False, default=None)
|
|
765
854
|
@click.option("--list", "list_only", is_flag=True, default=False,
|
|
766
855
|
help="Print the model list without switching.")
|
|
767
|
-
|
|
856
|
+
@click.option("--update-key", "update_key", is_flag=True, default=False,
|
|
857
|
+
help="Re-prompt for the provider API key even if one is already saved.")
|
|
858
|
+
def switch_model(model_name, list_only, update_key):
|
|
768
859
|
"""Switch the active LLM model for --ask.
|
|
769
860
|
|
|
770
861
|
MODEL_NAME: optional model string to switch directly (e.g. gpt-5.6-terra,
|
|
771
862
|
claude-sonnet-5, gemma4:e4b). Omit to show the interactive menu.
|
|
772
863
|
Any string containing '/' that is not a known named model is treated as a
|
|
773
864
|
custom OpenRouter model string.
|
|
865
|
+
|
|
866
|
+
To update/change your saved API key for a provider, run:
|
|
867
|
+
ycb switch model --update-key
|
|
774
868
|
"""
|
|
775
869
|
from src.qa.providers import MODELS, resolve_model_entry
|
|
776
870
|
|
|
871
|
+
current_provider = os.environ.get("YCB_LLM_PROVIDER", "local")
|
|
872
|
+
|
|
777
873
|
# ── --list: just print, don't switch ────────────────────────────────
|
|
778
874
|
if list_only:
|
|
779
875
|
_print_model_menu()
|
|
780
|
-
current_provider = os.environ.get("YCB_LLM_PROVIDER", "local")
|
|
781
876
|
current_model = os.environ.get("YCB_LLM_MODEL", "gemma4:e4b")
|
|
782
877
|
console.print(
|
|
783
878
|
f"[dim]Current: [bold]{current_model}[/bold] via {current_provider}[/dim]"
|
|
@@ -789,7 +884,10 @@ def switch_model(model_name, list_only):
|
|
|
789
884
|
# Check named models first
|
|
790
885
|
entry = resolve_model_entry(model_name)
|
|
791
886
|
if entry:
|
|
792
|
-
|
|
887
|
+
# Auto-force key update when switching to a different provider
|
|
888
|
+
switching_provider = entry.provider != current_provider
|
|
889
|
+
_apply_model_switch(entry.provider, entry.model_id,
|
|
890
|
+
force_key_update=update_key or switching_provider)
|
|
793
891
|
return
|
|
794
892
|
|
|
795
893
|
# Treat any slug with '/' as a custom OpenRouter model
|
|
@@ -797,7 +895,9 @@ def switch_model(model_name, list_only):
|
|
|
797
895
|
console.print(
|
|
798
896
|
f" [dim]Treating '{model_name}' as a custom OpenRouter model.[/dim]"
|
|
799
897
|
)
|
|
800
|
-
|
|
898
|
+
switching_provider = "openrouter" != current_provider
|
|
899
|
+
_apply_model_switch("openrouter", model_name,
|
|
900
|
+
force_key_update=update_key or switching_provider)
|
|
801
901
|
return
|
|
802
902
|
|
|
803
903
|
# Unknown string and no '/'
|
|
@@ -846,10 +946,14 @@ def switch_model(model_name, list_only):
|
|
|
846
946
|
console.print(" [yellow]Model string cannot be empty.[/yellow]")
|
|
847
947
|
continue
|
|
848
948
|
|
|
849
|
-
|
|
949
|
+
switching_provider = "openrouter" != current_provider
|
|
950
|
+
_apply_model_switch("openrouter", custom_model,
|
|
951
|
+
force_key_update=update_key or switching_provider)
|
|
850
952
|
return
|
|
851
953
|
|
|
852
|
-
|
|
954
|
+
switching_provider = entry.provider != current_provider
|
|
955
|
+
_apply_model_switch(entry.provider, entry.model_id,
|
|
956
|
+
force_key_update=update_key or switching_provider)
|
|
853
957
|
return
|
|
854
958
|
|
|
855
959
|
|
|
@@ -39,7 +39,7 @@ class AirtableConnector(BaseConnector):
|
|
|
39
39
|
def is_enabled(self) -> bool:
|
|
40
40
|
if not super().is_enabled():
|
|
41
41
|
return False
|
|
42
|
-
return
|
|
42
|
+
return self.are_valid_credentials(self.api_token)
|
|
43
43
|
|
|
44
44
|
def _headers(self) -> Dict[str, str]:
|
|
45
45
|
return {"Authorization": f"Bearer {self.api_token}", "Content-Type": "application/json"}
|
|
@@ -22,7 +22,7 @@ class MixpanelConnector(BaseConnector):
|
|
|
22
22
|
def is_enabled(self) -> bool:
|
|
23
23
|
if not super().is_enabled():
|
|
24
24
|
return False
|
|
25
|
-
return
|
|
25
|
+
return self.are_valid_credentials(self.api_secret)
|
|
26
26
|
|
|
27
27
|
def _auth(self) -> tuple:
|
|
28
28
|
return (self.api_secret or "", "")
|
|
@@ -86,7 +86,7 @@ class AmplitudeConnector(BaseConnector):
|
|
|
86
86
|
def is_enabled(self) -> bool:
|
|
87
87
|
if not super().is_enabled():
|
|
88
88
|
return False
|
|
89
|
-
return
|
|
89
|
+
return self.are_valid_credentials(self.api_key, self.secret_key)
|
|
90
90
|
|
|
91
91
|
def _auth(self) -> tuple:
|
|
92
92
|
return (self.api_key or "", self.secret_key or "")
|
|
@@ -37,7 +37,7 @@ class AshbyConnector(BaseConnector):
|
|
|
37
37
|
def is_enabled(self) -> bool:
|
|
38
38
|
if not super().is_enabled():
|
|
39
39
|
return False
|
|
40
|
-
return
|
|
40
|
+
return self.are_valid_credentials(self.api_key)
|
|
41
41
|
|
|
42
42
|
def ingest(self, days_back: int = 30) -> List[RawDataItem]:
|
|
43
43
|
if not self.is_enabled():
|
|
@@ -46,7 +46,7 @@ class BambooHRConnector(BaseConnector):
|
|
|
46
46
|
def is_enabled(self) -> bool:
|
|
47
47
|
if not super().is_enabled():
|
|
48
48
|
return False
|
|
49
|
-
return
|
|
49
|
+
return self.are_valid_credentials(self.api_key, self.subdomain)
|
|
50
50
|
|
|
51
51
|
def ingest(self, days_back: int = 30) -> List[RawDataItem]:
|
|
52
52
|
if not self.is_enabled():
|
|
@@ -21,6 +21,85 @@ class BaseConnector(ABC):
|
|
|
21
21
|
"""Fetch raw content from source API and map to RawDataItem schema"""
|
|
22
22
|
pass
|
|
23
23
|
|
|
24
|
+
@staticmethod
|
|
25
|
+
def is_valid_credential(value: Optional[str]) -> bool:
|
|
26
|
+
"""Check if a credential string is non-empty and not a dummy/template placeholder."""
|
|
27
|
+
if not value or not isinstance(value, str):
|
|
28
|
+
return False
|
|
29
|
+
val = value.strip()
|
|
30
|
+
if not val:
|
|
31
|
+
return False
|
|
32
|
+
val_lower = val.lower()
|
|
33
|
+
|
|
34
|
+
# Obvious placeholder indicators from .env.example / template files
|
|
35
|
+
placeholders = (
|
|
36
|
+
"your-user-token",
|
|
37
|
+
"your_user_token",
|
|
38
|
+
"your_token",
|
|
39
|
+
"your-token",
|
|
40
|
+
"your-company",
|
|
41
|
+
"your_company",
|
|
42
|
+
"yourcompany",
|
|
43
|
+
"your_personal_access_token",
|
|
44
|
+
"your_notion_integration_token",
|
|
45
|
+
"your_discord_bot_token",
|
|
46
|
+
"your_linear_key",
|
|
47
|
+
"your_azure",
|
|
48
|
+
"your_dropbox",
|
|
49
|
+
"your_mixpanel",
|
|
50
|
+
"your_amplitude",
|
|
51
|
+
"your_algolia",
|
|
52
|
+
"your_exa",
|
|
53
|
+
"your_perplexity",
|
|
54
|
+
"your_page_access_token",
|
|
55
|
+
"your_todoist",
|
|
56
|
+
"your_jira",
|
|
57
|
+
"your_asana",
|
|
58
|
+
"your_calendly",
|
|
59
|
+
"your_clickup",
|
|
60
|
+
"your_airtable",
|
|
61
|
+
"your_datadog",
|
|
62
|
+
"your_segment",
|
|
63
|
+
"your_front",
|
|
64
|
+
"your_zoom",
|
|
65
|
+
"your_x_twitter",
|
|
66
|
+
"your_hubspot",
|
|
67
|
+
"your_salesforce",
|
|
68
|
+
"your_monday",
|
|
69
|
+
"your_basecamp",
|
|
70
|
+
"your_ashby",
|
|
71
|
+
"your_bamboohr",
|
|
72
|
+
"your_deel",
|
|
73
|
+
"your_rippling",
|
|
74
|
+
"your_email",
|
|
75
|
+
"your_project_id",
|
|
76
|
+
"your_team_id",
|
|
77
|
+
"your_workspace_gid",
|
|
78
|
+
"owner/repo1",
|
|
79
|
+
"group/project1",
|
|
80
|
+
"you@yourcompany",
|
|
81
|
+
"placeholder",
|
|
82
|
+
"appxxxxxxxx",
|
|
83
|
+
"sk-...",
|
|
84
|
+
"sk-ant-...",
|
|
85
|
+
"sk-or-...",
|
|
86
|
+
"eaag...",
|
|
87
|
+
)
|
|
88
|
+
if any(p in val_lower for p in placeholders):
|
|
89
|
+
return False
|
|
90
|
+
if val_lower.startswith("your_") or val_lower.startswith("your-"):
|
|
91
|
+
return False
|
|
92
|
+
if "your_" in val_lower or "-your-" in val_lower or "-your_" in val_lower:
|
|
93
|
+
return False
|
|
94
|
+
return True
|
|
95
|
+
|
|
96
|
+
@classmethod
|
|
97
|
+
def are_valid_credentials(cls, *values: Optional[str]) -> bool:
|
|
98
|
+
"""Return True if all provided credential values are non-empty and not dummy placeholders."""
|
|
99
|
+
if not values:
|
|
100
|
+
return False
|
|
101
|
+
return all(cls.is_valid_credential(v) for v in values)
|
|
102
|
+
|
|
24
103
|
def is_enabled(self) -> bool:
|
|
25
104
|
"""Check if connector is enabled via CONNECTORS_ENABLED environment variable or credentials"""
|
|
26
105
|
enabled_env = os.getenv("CONNECTORS_ENABLED", "")
|
|
@@ -51,7 +51,7 @@ class BasecampConnector(BaseConnector):
|
|
|
51
51
|
def is_enabled(self) -> bool:
|
|
52
52
|
if not super().is_enabled():
|
|
53
53
|
return False
|
|
54
|
-
return
|
|
54
|
+
return self.are_valid_credentials(self.access_token, self.account_id)
|
|
55
55
|
|
|
56
56
|
def _headers(self) -> Dict[str, str]:
|
|
57
57
|
return {
|
|
@@ -35,7 +35,7 @@ class DatadogConnector(BaseConnector):
|
|
|
35
35
|
def is_enabled(self) -> bool:
|
|
36
36
|
if not super().is_enabled():
|
|
37
37
|
return False
|
|
38
|
-
return
|
|
38
|
+
return self.are_valid_credentials(self.api_key, self.app_key)
|
|
39
39
|
|
|
40
40
|
def _headers(self) -> Dict[str, str]:
|
|
41
41
|
return {
|
|
@@ -34,7 +34,7 @@ class DeelConnector(BaseConnector):
|
|
|
34
34
|
def is_enabled(self) -> bool:
|
|
35
35
|
if not super().is_enabled():
|
|
36
36
|
return False
|
|
37
|
-
return
|
|
37
|
+
return self.are_valid_credentials(self.api_token)
|
|
38
38
|
|
|
39
39
|
def ingest(self, days_back: int = 30) -> List[RawDataItem]:
|
|
40
40
|
if not self.is_enabled():
|
|
@@ -22,7 +22,7 @@ class FacebookConnector(BaseConnector):
|
|
|
22
22
|
def is_enabled(self) -> bool:
|
|
23
23
|
if not super().is_enabled():
|
|
24
24
|
return False
|
|
25
|
-
return
|
|
25
|
+
return self.are_valid_credentials(self.page_token)
|
|
26
26
|
|
|
27
27
|
def _params(self, extra: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
28
28
|
p = {"access_token": self.page_token or ""}
|
|
@@ -57,7 +57,7 @@ class HubSpotConnector(BaseConnector):
|
|
|
57
57
|
def is_enabled(self) -> bool:
|
|
58
58
|
if not super().is_enabled():
|
|
59
59
|
return False
|
|
60
|
-
return
|
|
60
|
+
return self.are_valid_credentials(self.access_token)
|
|
61
61
|
|
|
62
62
|
def _headers(self) -> Dict[str, str]:
|
|
63
63
|
return {
|
|
@@ -28,7 +28,7 @@ class JiraConnector(BaseConnector):
|
|
|
28
28
|
def is_enabled(self) -> bool:
|
|
29
29
|
if not super().is_enabled():
|
|
30
30
|
return False
|
|
31
|
-
return
|
|
31
|
+
return self.are_valid_credentials(self.domain, self.email, self.api_token)
|
|
32
32
|
|
|
33
33
|
def _headers(self) -> Dict[str, str]:
|
|
34
34
|
auth_str = f"{self.email}:{self.api_token}"
|
|
@@ -33,8 +33,8 @@ class OutlookTeamsConnector(BaseConnector):
|
|
|
33
33
|
def is_enabled(self) -> bool:
|
|
34
34
|
if not super().is_enabled():
|
|
35
35
|
return False
|
|
36
|
-
# Enabled if client_id and client_secret are configured
|
|
37
|
-
return
|
|
36
|
+
# Enabled if client_id and client_secret are configured and non-placeholder
|
|
37
|
+
return self.are_valid_credentials(self.client_id, self.client_secret)
|
|
38
38
|
|
|
39
39
|
def _get_access_token(self) -> Optional[str]:
|
|
40
40
|
"""Obtain Graph API OAuth2 token using client credentials flow"""
|
|
@@ -40,7 +40,7 @@ class RipplingConnector(BaseConnector):
|
|
|
40
40
|
def is_enabled(self) -> bool:
|
|
41
41
|
if not super().is_enabled():
|
|
42
42
|
return False
|
|
43
|
-
return
|
|
43
|
+
return self.are_valid_credentials(self.api_token)
|
|
44
44
|
|
|
45
45
|
def ingest(self, days_back: int = 30) -> List[RawDataItem]:
|
|
46
46
|
if not self.is_enabled():
|
|
@@ -76,7 +76,7 @@ class SalesforceConnector(BaseConnector):
|
|
|
76
76
|
def is_enabled(self) -> bool:
|
|
77
77
|
if not super().is_enabled():
|
|
78
78
|
return False
|
|
79
|
-
return
|
|
79
|
+
return self.are_valid_credentials(self.client_id, self.client_secret)
|
|
80
80
|
|
|
81
81
|
def _authenticate(self) -> None:
|
|
82
82
|
"""Exchange client credentials for an access token and instance URL."""
|
|
@@ -22,7 +22,7 @@ class AlgoliaConnector(BaseConnector):
|
|
|
22
22
|
def is_enabled(self) -> bool:
|
|
23
23
|
if not super().is_enabled():
|
|
24
24
|
return False
|
|
25
|
-
return
|
|
25
|
+
return self.are_valid_credentials(self.app_id, self.api_key, self.index_name)
|
|
26
26
|
|
|
27
27
|
def _headers(self) -> Dict[str, str]:
|
|
28
28
|
return {
|
|
@@ -95,7 +95,7 @@ class ExaConnector(BaseConnector):
|
|
|
95
95
|
def is_enabled(self) -> bool:
|
|
96
96
|
if not super().is_enabled():
|
|
97
97
|
return False
|
|
98
|
-
return
|
|
98
|
+
return self.are_valid_credentials(self.api_key)
|
|
99
99
|
|
|
100
100
|
def _headers(self) -> Dict[str, str]:
|
|
101
101
|
return {
|
|
@@ -172,7 +172,7 @@ class PerplexityConnector(BaseConnector):
|
|
|
172
172
|
def is_enabled(self) -> bool:
|
|
173
173
|
if not super().is_enabled():
|
|
174
174
|
return False
|
|
175
|
-
return
|
|
175
|
+
return self.are_valid_credentials(self.api_key)
|
|
176
176
|
|
|
177
177
|
def _headers(self) -> Dict[str, str]:
|
|
178
178
|
return {
|
|
@@ -31,7 +31,7 @@ class SegmentConnector(BaseConnector):
|
|
|
31
31
|
def is_enabled(self) -> bool:
|
|
32
32
|
if not super().is_enabled():
|
|
33
33
|
return False
|
|
34
|
-
return
|
|
34
|
+
return self.are_valid_credentials(self.access_token)
|
|
35
35
|
|
|
36
36
|
def _headers(self) -> Dict[str, str]:
|
|
37
37
|
return {
|
|
@@ -30,8 +30,12 @@ class SlackConnector(BaseConnector):
|
|
|
30
30
|
def is_enabled(self) -> bool:
|
|
31
31
|
if not super().is_enabled():
|
|
32
32
|
return False
|
|
33
|
-
# Only enabled if a valid per-user token (xoxp-) is configured
|
|
34
|
-
return bool(
|
|
33
|
+
# Only enabled if a valid per-user token (xoxp-) is configured and not a dummy/template placeholder
|
|
34
|
+
return bool(
|
|
35
|
+
self.user_token
|
|
36
|
+
and self.user_token.startswith("xoxp-")
|
|
37
|
+
and self.is_valid_credential(self.user_token)
|
|
38
|
+
)
|
|
35
39
|
|
|
36
40
|
def ingest(self, days_back: int = 30) -> List[RawDataItem]:
|
|
37
41
|
return self.ingest_slack(days_back=days_back)
|
|
@@ -67,7 +67,7 @@ class TwitterConnector(BaseConnector):
|
|
|
67
67
|
def is_enabled(self) -> bool:
|
|
68
68
|
if not super().is_enabled():
|
|
69
69
|
return False
|
|
70
|
-
return
|
|
70
|
+
return self.are_valid_credentials(self.bearer_token)
|
|
71
71
|
|
|
72
72
|
def _headers(self) -> Dict[str, str]:
|
|
73
73
|
return {"Authorization": f"Bearer {self.bearer_token}"}
|
|
@@ -42,7 +42,7 @@ class ZoomConnector(BaseConnector):
|
|
|
42
42
|
def is_enabled(self) -> bool:
|
|
43
43
|
if not super().is_enabled():
|
|
44
44
|
return False
|
|
45
|
-
return
|
|
45
|
+
return self.are_valid_credentials(self.account_id, self.client_id, self.client_secret)
|
|
46
46
|
|
|
47
47
|
def _fetch_access_token(self) -> str:
|
|
48
48
|
"""Exchange Server-to-Server credentials for a bearer access token."""
|
|
@@ -40,18 +40,11 @@ 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
|
|
|
45
|
+
# Recognized license tiers (including 'test' which receives full enterprise-level access)
|
|
46
|
+
VALID_TIERS = set(TIER_CONNECTORS.keys()) | {"test"}
|
|
47
|
+
|
|
55
48
|
# Categorized grouping of all 38 connectors for clean CLI status display
|
|
56
49
|
CONNECTOR_CATEGORIES: Dict[str, List[str]] = {
|
|
57
50
|
"Communication & Chat": ["slack", "discord", "outlook", "front", "zoom", "twitter", "facebook", "gmail"],
|
|
@@ -104,6 +97,110 @@ CONNECTOR_ENV_VARS: Dict[str, List[str]] = {
|
|
|
104
97
|
"rippling": ["RIPPLING_API_TOKEN"],
|
|
105
98
|
}
|
|
106
99
|
|
|
100
|
+
# Where to get credentials — env var -> (label, url)
|
|
101
|
+
# Shown as a hint before each credential prompt in `ycb connect`.
|
|
102
|
+
CONNECTOR_CREDENTIAL_LINKS: Dict[str, Tuple[str, str]] = {
|
|
103
|
+
# Slack
|
|
104
|
+
"SLACK_CLIENT_ID": ("Slack App Dashboard", "https://api.slack.com/apps"),
|
|
105
|
+
"SLACK_CLIENT_SECRET": ("Slack App Dashboard", "https://api.slack.com/apps"),
|
|
106
|
+
"SLACK_USER_TOKEN": ("Slack App OAuth & Permissions", "https://api.slack.com/apps"),
|
|
107
|
+
# Google (shared across google_docs, gmail, sheets, drive)
|
|
108
|
+
"GOOGLE_CREDENTIALS_PATH": ("Google Cloud Console — OAuth credentials (download as credentials.json)",
|
|
109
|
+
"https://console.cloud.google.com/apis/credentials"),
|
|
110
|
+
# GitHub
|
|
111
|
+
"GITHUB_TOKEN": ("GitHub Personal Access Tokens", "https://github.com/settings/tokens"),
|
|
112
|
+
"GITHUB_REPOS": ("Your GitHub repo list", "e.g. myorg/repo1,myorg/repo2"),
|
|
113
|
+
# Notion
|
|
114
|
+
"NOTION_TOKEN": ("Notion My Integrations", "https://www.notion.so/my-integrations"),
|
|
115
|
+
# Discord
|
|
116
|
+
"DISCORD_BOT_TOKEN": ("Discord Developer Portal", "https://discord.com/developers/applications"),
|
|
117
|
+
"DISCORD_GUILD_IDS": ("Your Discord Server IDs", "Right-click server → Copy Server ID (enable Developer Mode first)"),
|
|
118
|
+
# Linear
|
|
119
|
+
"LINEAR_API_KEY": ("Linear API Settings", "https://linear.app/settings/api"),
|
|
120
|
+
# Outlook / Microsoft Graph
|
|
121
|
+
"MS_CLIENT_ID": ("Azure App Registrations", "https://portal.azure.com/#view/Microsoft_AAD_RegisteredApps"),
|
|
122
|
+
"MS_CLIENT_SECRET": ("Azure App — Certificates & Secrets", "https://portal.azure.com/#view/Microsoft_AAD_RegisteredApps"),
|
|
123
|
+
"MS_TENANT_ID": ("Azure Active Directory — Tenant ID", "https://portal.azure.com/#view/Microsoft_AAD_IAM/ActiveDirectoryMenuBlade"),
|
|
124
|
+
# GitLab
|
|
125
|
+
"GITLAB_TOKEN": ("GitLab Personal Access Tokens", "https://gitlab.com/-/profile/personal_access_tokens"),
|
|
126
|
+
"GITLAB_URL": ("Your GitLab instance URL", "e.g. https://gitlab.com or your self-hosted URL"),
|
|
127
|
+
"GITLAB_PROJECTS": ("Your GitLab project list", "e.g. mygroup/project1,mygroup/project2"),
|
|
128
|
+
# Dropbox
|
|
129
|
+
"DROPBOX_TOKEN": ("Dropbox App Console", "https://www.dropbox.com/developers/apps"),
|
|
130
|
+
# Mixpanel
|
|
131
|
+
"MIXPANEL_API_SECRET": ("Mixpanel Project Settings — Service Account", "https://mixpanel.com/settings/project"),
|
|
132
|
+
"MIXPANEL_PROJECT_ID": ("Mixpanel Project Settings", "https://mixpanel.com/settings/project"),
|
|
133
|
+
# Amplitude
|
|
134
|
+
"AMPLITUDE_API_KEY": ("Amplitude Project Settings", "https://analytics.amplitude.com/login"),
|
|
135
|
+
"AMPLITUDE_SECRET_KEY": ("Amplitude Project Settings", "https://analytics.amplitude.com/login"),
|
|
136
|
+
# Algolia
|
|
137
|
+
"ALGOLIA_APP_ID": ("Algolia Dashboard — API Keys", "https://dashboard.algolia.com/account/api-keys"),
|
|
138
|
+
"ALGOLIA_API_KEY": ("Algolia Dashboard — API Keys", "https://dashboard.algolia.com/account/api-keys"),
|
|
139
|
+
"ALGOLIA_INDEX_NAME": ("Your Algolia index name", "Found in Algolia Dashboard → Search → Indices"),
|
|
140
|
+
# Exa
|
|
141
|
+
"EXA_API_KEY": ("Exa Dashboard", "https://dashboard.exa.ai/api-keys"),
|
|
142
|
+
# Perplexity
|
|
143
|
+
"PERPLEXITY_API_KEY": ("Perplexity API Settings", "https://www.perplexity.ai/settings/api"),
|
|
144
|
+
# Facebook
|
|
145
|
+
"FACEBOOK_PAGE_TOKEN": ("Facebook Developer Portal — Page Access Token", "https://developers.facebook.com/apps"),
|
|
146
|
+
"FACEBOOK_PAGE_ID": ("Your Facebook Page ID", "Found in Facebook Page → About or Page Settings"),
|
|
147
|
+
# Todoist
|
|
148
|
+
"TODOIST_API_TOKEN": ("Todoist Integrations Settings", "https://app.todoist.com/app/settings/integrations/developer"),
|
|
149
|
+
# Web Crawler
|
|
150
|
+
"WEB_CRAWLER_START_URLS": ("Comma-separated list of URLs to crawl", "e.g. https://docs.yourcompany.com,https://yourcompany.com/blog"),
|
|
151
|
+
# Jira / Atlassian
|
|
152
|
+
"JIRA_DOMAIN": ("Your Jira workspace domain", "e.g. yourcompany.atlassian.net"),
|
|
153
|
+
"JIRA_EMAIL": ("Your Atlassian account email", "The email you use to log in to Jira"),
|
|
154
|
+
"JIRA_API_TOKEN": ("Atlassian API Tokens", "https://id.atlassian.com/manage-profile/security/api-tokens"),
|
|
155
|
+
# Asana
|
|
156
|
+
"ASANA_TOKEN": ("Asana Developer Console — Personal Access Token", "https://app.asana.com/0/developer-console"),
|
|
157
|
+
"ASANA_WORKSPACE_GID": ("Your Asana Workspace GID", "Found via GET https://app.asana.com/api/1.0/workspaces"),
|
|
158
|
+
# Calendly
|
|
159
|
+
"CALENDLY_TOKEN": ("Calendly Developer Portal — Personal Access Token", "https://developer.calendly.com/how-to-authenticate-with-personal-access-tokens"),
|
|
160
|
+
"CALENDLY_USER_URI": ("Your Calendly User URI", "Found via GET https://api.calendly.com/users/me"),
|
|
161
|
+
# ClickUp
|
|
162
|
+
"CLICKUP_API_TOKEN": ("ClickUp Settings — Apps", "https://app.clickup.com/settings/apps"),
|
|
163
|
+
"CLICKUP_TEAM_ID": ("Your ClickUp Team ID", "Found in ClickUp URL: app.clickup.com/<team_id>/"),
|
|
164
|
+
# Airtable
|
|
165
|
+
"AIRTABLE_TOKEN": ("Airtable Developer Hub — Personal Access Tokens", "https://airtable.com/create/tokens"),
|
|
166
|
+
"AIRTABLE_BASE_IDS": ("Your Airtable Base IDs", "Found in Airtable API docs page for each base: airtable.com/appXXXXXX/api/docs"),
|
|
167
|
+
# Datadog
|
|
168
|
+
"DATADOG_API_KEY": ("Datadog Organization Settings — API Keys", "https://app.datadoghq.com/organization-settings/api-keys"),
|
|
169
|
+
"DATADOG_APP_KEY": ("Datadog Organization Settings — Application Keys", "https://app.datadoghq.com/organization-settings/application-keys"),
|
|
170
|
+
# Segment
|
|
171
|
+
"SEGMENT_PERSONAS_TOKEN": ("Segment App — Access Tokens", "https://app.segment.com/goto-my-workspace/settings/access-management"),
|
|
172
|
+
"SEGMENT_SPACE_ID": ("Your Segment Space ID", "Found in Segment → Engage → Settings → API Access"),
|
|
173
|
+
# Front
|
|
174
|
+
"FRONT_API_TOKEN": ("Front App Settings — API Tokens", "https://app.frontapp.com/settings/tools/api"),
|
|
175
|
+
# Zoom
|
|
176
|
+
"ZOOM_ACCOUNT_ID": ("Zoom Marketplace — Server-to-Server OAuth App", "https://marketplace.zoom.us/develop/create"),
|
|
177
|
+
"ZOOM_CLIENT_ID": ("Zoom Marketplace — Server-to-Server OAuth App", "https://marketplace.zoom.us/develop/create"),
|
|
178
|
+
"ZOOM_CLIENT_SECRET": ("Zoom Marketplace — Server-to-Server OAuth App", "https://marketplace.zoom.us/develop/create"),
|
|
179
|
+
# Twitter / X
|
|
180
|
+
"TWITTER_BEARER_TOKEN": ("Twitter Developer Portal — Project/App Keys", "https://developer.twitter.com/en/portal/dashboard"),
|
|
181
|
+
"TWITTER_SEARCH_QUERIES": ("Comma-separated search queries", "e.g. @yourcompany,#yourproduct"),
|
|
182
|
+
# HubSpot
|
|
183
|
+
"HUBSPOT_ACCESS_TOKEN": ("HubSpot Private App Access Token", "https://app.hubspot.com/private-apps"),
|
|
184
|
+
# Salesforce
|
|
185
|
+
"SALESFORCE_CLIENT_ID": ("Salesforce Connected App Consumer Key", "Setup → Apps → App Manager → New Connected App"),
|
|
186
|
+
"SALESFORCE_CLIENT_SECRET": ("Salesforce Connected App Consumer Secret", "Setup → Apps → App Manager → New Connected App"),
|
|
187
|
+
"SALESFORCE_DOMAIN": ("Your Salesforce domain", "e.g. yourcompany.my.salesforce.com"),
|
|
188
|
+
# Monday
|
|
189
|
+
"MONDAY_API_TOKEN": ("Monday.com Developer Center — API Token", "https://auth.monday.com/users/sign_in (Profile → Developers → API)"),
|
|
190
|
+
# Basecamp
|
|
191
|
+
"BASECAMP_ACCESS_TOKEN": ("Basecamp — Launchpad OAuth / Personal Tokens", "https://launchpad.37signals.com/"),
|
|
192
|
+
"BASECAMP_ACCOUNT_ID": ("Your Basecamp Account ID", "Found in your Basecamp URL: 3.basecamp.com/<account_id>/"),
|
|
193
|
+
# Ashby
|
|
194
|
+
"ASHBY_API_KEY": ("Ashby Settings — API Keys", "https://app.ashbyhq.com/admin/api/keys"),
|
|
195
|
+
# BambooHR
|
|
196
|
+
"BAMBOOHR_API_KEY": ("BambooHR API Access — Generate API Key", "https://app.bamboohr.com/settings/permissions/"),
|
|
197
|
+
"BAMBOOHR_SUBDOMAIN": ("Your BambooHR subdomain", "e.g. if your URL is mycompany.bamboohr.com, enter 'mycompany'"),
|
|
198
|
+
# Deel
|
|
199
|
+
"DEEL_API_TOKEN": ("Deel API Settings", "https://app.deel.com/settings/api"),
|
|
200
|
+
# Rippling
|
|
201
|
+
"RIPPLING_API_TOKEN": ("Rippling API Credentials", "https://app.rippling.com/api"),
|
|
202
|
+
}
|
|
203
|
+
|
|
107
204
|
|
|
108
205
|
def get_cached_plan_tier(cache_dir: Optional[str] = None, expected_key: Optional[str] = None) -> str:
|
|
109
206
|
"""Read the plan_tier from ~/.company-brain/license.json. Defaults to 'solo' if unconfigured.
|
|
@@ -165,8 +262,9 @@ def get_cached_plan_tier(cache_dir: Optional[str] = None, expected_key: Optional
|
|
|
165
262
|
except Exception:
|
|
166
263
|
pass
|
|
167
264
|
|
|
168
|
-
|
|
169
|
-
|
|
265
|
+
tier_lower = (tier or "").lower()
|
|
266
|
+
if not is_stale_or_expired and tier_lower in VALID_TIERS:
|
|
267
|
+
return tier_lower
|
|
170
268
|
|
|
171
269
|
# 3. Mismatch detected or no matching cache entry for resolved_key
|
|
172
270
|
if resolved_key:
|
|
@@ -182,7 +280,7 @@ def get_cached_plan_tier(cache_dir: Optional[str] = None, expected_key: Optional
|
|
|
182
280
|
)
|
|
183
281
|
if val_res and val_res.is_valid and val_res.plan_tier:
|
|
184
282
|
healed_tier = val_res.plan_tier.lower()
|
|
185
|
-
if healed_tier in
|
|
283
|
+
if healed_tier in VALID_TIERS:
|
|
186
284
|
return healed_tier
|
|
187
285
|
elif val_res and val_res.status == "expired":
|
|
188
286
|
try:
|
|
@@ -217,7 +315,8 @@ def get_cached_plan_tier(cache_dir: Optional[str] = None, expected_key: Optional
|
|
|
217
315
|
|
|
218
316
|
def is_connector_permitted(connector_name: str, plan_tier: str) -> bool:
|
|
219
317
|
"""Check if a connector is permitted in the given plan tier."""
|
|
220
|
-
|
|
318
|
+
effective_tier = "enterprise" if plan_tier.lower() == "test" else plan_tier.lower()
|
|
319
|
+
permitted = TIER_CONNECTORS.get(effective_tier, [])
|
|
221
320
|
return connector_name.lower() in permitted
|
|
222
321
|
|
|
223
322
|
|
|
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
|