mirror of
https://github.com/open-jarvis/OpenJarvis.git
synced 2026-07-27 21:05:34 +00:00
feat: overhaul install + setup UX for data sources, messaging, and desktop app
- Skip SetupWizard on launch, go straight to Chat page - Add Data Sources page with sidebar nav (separate tabs for data sources + messaging channels) - Add "Connect your data" banner + quick-action buttons on Chat empty state - Add hint on deep research agent pointing to Data Sources + Messaging tabs - Consistent naming: "Data Sources" and "Messaging Channels" everywhere - Fix Apple Notes / iMessage setup (remove broken system prefs link) - Fix Slack data source: auto-join public channels, rate limit retry, is_member filter - Add channels:join scope to all Slack manifests + docs - Fix Gmail: use gmail_imap connector (IMAP + app password), increase limit to 5000 - Fix sync endpoint: run in background thread, return immediately - Show sync progress with progress bar, error messages, Sync Now / Re-sync / Retry buttons - Add triggerSync() API + SyncStatusDisplay component - Rewrite all connector setup instructions with precise click-by-click steps - Notion: share all pages at once via top-level page sharing - Obsidian: show how to find vault path via Obsidian UI or Finder - SendBlue: add link to API Credentials page, add ngrok webhook step - Slack messaging: add Copy button for JSON manifest - Add OpenJarvis Slack icon asset - Shorten deep research template description - Fix desktop app port (8222 → 8000 to match server default) - Disable auto-updater for local dev builds - Register Slack, Outlook, GCalendar connectors in __init__.py - Auto-create default agent when setting up messaging channels Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
09d1cc3681
commit
a57813fcfa
Binary file not shown.
|
After Width: | Height: | Size: 100 KiB |
@@ -7,7 +7,7 @@ use tauri_plugin_autostart::MacosLauncher;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
const OLLAMA_PORT: u16 = 11434;
|
||||
const JARVIS_PORT: u16 = 8222;
|
||||
const JARVIS_PORT: u16 = 8000;
|
||||
|
||||
/// Small, fast model pulled at startup so the app opens quickly.
|
||||
const STARTUP_MODEL: &str = "qwen3.5:4b";
|
||||
@@ -1203,7 +1203,7 @@ pub fn run() {
|
||||
MacosLauncher::LaunchAgent,
|
||||
Some(vec!["--hidden"]),
|
||||
))
|
||||
.plugin(tauri_plugin_updater::Builder::new().build())
|
||||
// .plugin(tauri_plugin_updater::Builder::new().build()) // disabled for local dev
|
||||
.plugin(tauri_plugin_process::init())
|
||||
.plugin(tauri_plugin_single_instance::init(|app, _args, _cwd| {
|
||||
if let Some(window) = app.get_webview_window("main") {
|
||||
|
||||
@@ -58,10 +58,9 @@
|
||||
},
|
||||
"plugins": {
|
||||
"updater": {
|
||||
"active": false,
|
||||
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDFFNzUzMzhEOEY2MjNEMDMKUldRRFBXS1BqVE4xSG8vK0lkUWN4WnZQYVIrbmc4RmpoOGlJWTBLTE15RlIya3JvQisvdUR3a0QK",
|
||||
"endpoints": [
|
||||
"https://github.com/open-jarvis/OpenJarvis/releases/download/desktop-latest/latest.json"
|
||||
]
|
||||
"endpoints": []
|
||||
},
|
||||
"deep-link": {
|
||||
"desktop": {
|
||||
|
||||
@@ -241,54 +241,19 @@ Same as Google Drive — use the same Google Cloud project and OAuth client.
|
||||
|
||||
Slack serves two purposes in OpenJarvis:
|
||||
|
||||
- **Data connector** — indexes channel messages and threads so your agent can search them
|
||||
- **Data source** — indexes channel messages, DMs, and threads so your agent can search them
|
||||
- **Messaging channel** — lets you DM your agent directly in Slack
|
||||
|
||||
### Setup: Data Connector (read Slack messages)
|
||||
We recommend creating **one Slack app** that handles both. The App Manifest below includes all scopes needed.
|
||||
|
||||
1. **Go to Slack App Settings:**
|
||||
[Open Slack Apps →](https://api.slack.com/apps)
|
||||
### Quick Setup (App Manifest — recommended)
|
||||
|
||||
2. **Create a new app:**
|
||||
- Click "Create New App" → "From scratch"
|
||||
- Name it (e.g. "OpenJarvis") and select your workspace
|
||||
1. **Go to [Slack App Settings →](https://api.slack.com/apps)**
|
||||
|
||||
3. **Add Bot Token Scopes** (OAuth & Permissions → Bot Token Scopes):
|
||||
2. **Create New App → "From an app manifest"** → select your workspace
|
||||
|
||||
| Scope | Purpose |
|
||||
|-------|---------|
|
||||
| `chat:write` | Send messages |
|
||||
| `im:write` | Open DM conversations |
|
||||
| `im:read` | List DM conversations |
|
||||
| `im:history` | Read DM history + receive DM events |
|
||||
| `users:read` | Look up user info |
|
||||
| `channels:read` | List public channels |
|
||||
| `channels:history` | Read public channel messages |
|
||||
| `app_mentions:read` | See @mentions of the bot |
|
||||
3. **Paste this JSON manifest** (includes all scopes for data source + messaging):
|
||||
|
||||
4. **Install to workspace:**
|
||||
- Click "Install to Workspace" → Authorize
|
||||
- Copy the **Bot User OAuth Token** (starts with `xoxb-`)
|
||||
|
||||
5. **Connect in OpenJarvis:**
|
||||
- Desktop/Browser: Agents → **Channels** tab → Slack → paste the bot token
|
||||
- CLI: `uv run jarvis connect slack`
|
||||
|
||||
### Setup: Messaging Channel (DM your agent in Slack)
|
||||
|
||||
To DM your agent and get research responses, you need Socket Mode + Event Subscriptions:
|
||||
|
||||
1. **Enable Socket Mode:**
|
||||
- Go to your Slack app settings → **Socket Mode** (left sidebar)
|
||||
- Toggle **"Enable Socket Mode"** to ON
|
||||
- Click **"Generate"** to create an App-Level Token
|
||||
- Add the `connections:write` scope
|
||||
- Name it anything (e.g. "socket") → Generate
|
||||
- Copy the token (starts with `xapp-`)
|
||||
|
||||
2. **Set up Event Subscriptions:**
|
||||
- Go to **App Manifest** (left sidebar) — this is the most reliable method
|
||||
- Replace the entire manifest with:
|
||||
```json
|
||||
{
|
||||
"display_information": { "name": "OpenJarvis" },
|
||||
@@ -303,43 +268,79 @@ To DM your agent and get research responses, you need Socket Mode + Event Subscr
|
||||
"oauth_config": {
|
||||
"scopes": {
|
||||
"bot": [
|
||||
"chat:write", "im:write", "im:read", "im:history",
|
||||
"users:read", "channels:read", "channels:history",
|
||||
"app_mentions:read", "assistant:write"
|
||||
"channels:read", "channels:history", "channels:join",
|
||||
"groups:read", "groups:history",
|
||||
"im:read", "im:write", "im:history",
|
||||
"mpim:read", "mpim:history",
|
||||
"chat:write",
|
||||
"users:read",
|
||||
"app_mentions:read"
|
||||
]
|
||||
},
|
||||
"pkce_enabled": false
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"event_subscriptions": { "bot_events": ["message.im"] },
|
||||
"org_deploy_enabled": false,
|
||||
"socket_mode_enabled": true,
|
||||
"token_rotation_enabled": false
|
||||
"socket_mode_enabled": true
|
||||
}
|
||||
}
|
||||
```
|
||||
- Click **Save Changes**
|
||||
|
||||
3. **Reinstall the app:**
|
||||
- Go to **Install App** → click **"Reinstall to Workspace"**
|
||||
- This is required after changing scopes or event subscriptions
|
||||
- Copy the **new Bot User OAuth Token** (it may change after reinstall)
|
||||
4. **Review and click Create**
|
||||
|
||||
4. **Connect in OpenJarvis:**
|
||||
- Desktop/Browser: Agents → **Messaging** tab → Slack → Set Up
|
||||
- Enter the **Bot Token** (`xoxb-...`) and **App Token** (`xapp-...`)
|
||||
- Click Connect
|
||||
5. **Install the app:** Install App → Install to Workspace → Authorize
|
||||
|
||||
5. **DM your agent:**
|
||||
- In Slack, find **OpenJarvis** under Apps (or Direct Messages)
|
||||
- If you don't see it: click **"+"** next to Direct Messages → search "OpenJarvis"
|
||||
- Send a message → the agent responds with "Message received! Researching now..." then the full research answer
|
||||
6. **Copy the Bot Token:** Go to OAuth & Permissions → copy the **Bot User OAuth Token** (`xoxb-...`)
|
||||
|
||||
7. **Create an App-Level Token (for DMs):**
|
||||
- Go to Basic Information → App-Level Tokens → Generate Token
|
||||
- Name it "socket" → add the `connections:write` scope → Generate
|
||||
- Copy the token (`xapp-...`)
|
||||
|
||||
8. **(Optional) Set the app icon:**
|
||||
- Go to Basic Information → Display Information
|
||||
- Upload the [OpenJarvis icon](https://github.com/open-jarvis/OpenJarvis/blob/main/assets/openjarvis-slack-icon.jpg)
|
||||
|
||||
### Required Bot Token Scopes (reference)
|
||||
|
||||
| Scope | Purpose |
|
||||
|-------|---------|
|
||||
| `channels:read` | List public channels |
|
||||
| `channels:history` | Read public channel messages |
|
||||
| `channels:join` | Auto-join public channels for indexing |
|
||||
| `groups:read` | List private channels |
|
||||
| `groups:history` | Read private channel messages |
|
||||
| `im:read` | List DM conversations |
|
||||
| `im:write` | Open DM conversations |
|
||||
| `im:history` | Read DM history + receive DM events |
|
||||
| `mpim:read` | List group DMs |
|
||||
| `mpim:history` | Read group DM messages |
|
||||
| `chat:write` | Send messages and responses |
|
||||
| `users:read` | Look up user info |
|
||||
| `app_mentions:read` | See @mentions of the bot |
|
||||
|
||||
**App-Level Token scope:** `connections:write` (required for Socket Mode / DMs)
|
||||
|
||||
### Connecting in OpenJarvis
|
||||
|
||||
**As a data source** (read channel messages):
|
||||
- Desktop/Browser: Data Sources → Slack → paste the bot token (`xoxb-...`)
|
||||
- CLI: `uv run jarvis connect slack`
|
||||
|
||||
**As a messaging channel** (DM your agent):
|
||||
- Desktop/Browser: Data Sources → Messaging Channels → Slack → Set Up
|
||||
- Enter both the **Bot Token** (`xoxb-...`) and **App Token** (`xapp-...`)
|
||||
- Or: Agents → select agent → Messaging Channels → Slack → Set Up
|
||||
|
||||
**DM your agent:**
|
||||
- In Slack, find **OpenJarvis** under Apps (or Direct Messages)
|
||||
- If you don't see it: click "+" next to Direct Messages → search "OpenJarvis"
|
||||
- Send a message → the agent responds in a thread
|
||||
|
||||
### Important Notes
|
||||
|
||||
- **Reinstall after scope changes:** Every time you add new scopes or change event subscriptions, you MUST reinstall the app. Otherwise the changes don't take effect.
|
||||
- **Don't use the Event Subscriptions UI for Request URL:** With Socket Mode enabled, you don't need a Request URL. If the Event Subscriptions page asks for one, use the **App Manifest** method instead (step 2 above).
|
||||
- **App-Level Token vs Bot Token:** These are different. The Bot Token (`xoxb-`) is for API calls. The App Token (`xapp-`) is for Socket Mode. You need both for DMs to work.
|
||||
- **Reinstall after scope changes:** Every time you add new scopes or change event subscriptions, you MUST reinstall the app.
|
||||
- **App-Level Token vs Bot Token:** The Bot Token (`xoxb-`) is for API calls. The App Token (`xapp-`) is for Socket Mode. You need both for DMs to work.
|
||||
- **Channel visibility:** The bot can only read channels it's been added to. Invite it with `/invite @OpenJarvis` in each channel you want indexed.
|
||||
- **Thread replies:** If you reply in a thread, the bot sees it. New top-level messages also work.
|
||||
|
||||
### Troubleshooting
|
||||
@@ -351,8 +352,7 @@ To DM your agent and get research responses, you need Socket Mode + Event Subscr
|
||||
| Bot doesn't respond to DMs | Make sure Socket Mode is enabled, `message.im` event is subscribed, and the app was reinstalled after changes |
|
||||
| "missing_scope" error | Add the missing scope in OAuth & Permissions → Reinstall the app |
|
||||
| Bot not visible in Slack | Go to Install App → Reinstall to Workspace |
|
||||
| No messages found (data connector) | The bot can only see channels it's been added to. Invite it: `/invite @OpenJarvis` in the channel |
|
||||
| Event Subscriptions won't save without Request URL | Use the App Manifest method instead (see step 2) — it bypasses the URL verification |
|
||||
| No messages found (data source) | The bot can only see channels it's been added to. Invite it: `/invite @OpenJarvis` in the channel |
|
||||
| Socket Mode connects but no events received | Verify `message.im` is in the manifest's `bot_events`, reinstall the app |
|
||||
|
||||
---
|
||||
|
||||
+30
-30
@@ -6,6 +6,7 @@ import { DashboardPage } from './pages/DashboardPage';
|
||||
import { SettingsPage } from './pages/SettingsPage';
|
||||
import { GetStartedPage } from './pages/GetStartedPage';
|
||||
import { AgentsPage } from './pages/AgentsPage';
|
||||
import { DataSourcesPage } from './pages/DataSourcesPage';
|
||||
import { LogsPage } from './pages/LogsPage';
|
||||
import { CommandPalette } from './components/CommandPalette';
|
||||
import { SetupScreen } from './components/SetupScreen';
|
||||
@@ -124,36 +125,34 @@ export default function App() {
|
||||
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||
}, [commandPaletteOpen, setCommandPaletteOpen, toggleSystemPanel]);
|
||||
|
||||
// Desktop auto-update check (runs once on launch)
|
||||
const updateChecked = useRef(false);
|
||||
useEffect(() => {
|
||||
if (!isTauri() || updateChecked.current) return;
|
||||
updateChecked.current = true;
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
const { check } = await import('@tauri-apps/plugin-updater');
|
||||
const update = await check();
|
||||
if (update) {
|
||||
await update.downloadAndInstall();
|
||||
const { toast } = await import('sonner');
|
||||
toast.info('Update ready', {
|
||||
description: 'A new version has been downloaded. Restart to apply.',
|
||||
duration: Infinity,
|
||||
action: {
|
||||
label: 'Restart Now',
|
||||
onClick: async () => {
|
||||
const { relaunch } = await import('@tauri-apps/plugin-process');
|
||||
await relaunch();
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
// Silent — no internet or endpoint issue
|
||||
}
|
||||
})();
|
||||
}, []);
|
||||
// Desktop auto-update check — disabled during local development.
|
||||
// Re-enable for production releases by uncommenting below.
|
||||
// const updateChecked = useRef(false);
|
||||
// useEffect(() => {
|
||||
// if (!isTauri() || updateChecked.current) return;
|
||||
// updateChecked.current = true;
|
||||
// (async () => {
|
||||
// try {
|
||||
// const { check } = await import('@tauri-apps/plugin-updater');
|
||||
// const update = await check();
|
||||
// if (update) {
|
||||
// await update.downloadAndInstall();
|
||||
// const { toast } = await import('sonner');
|
||||
// toast.info('Update ready', {
|
||||
// description: 'A new version has been downloaded. Restart to apply.',
|
||||
// duration: Infinity,
|
||||
// action: {
|
||||
// label: 'Restart Now',
|
||||
// onClick: async () => {
|
||||
// const { relaunch } = await import('@tauri-apps/plugin-process');
|
||||
// await relaunch();
|
||||
// },
|
||||
// },
|
||||
// });
|
||||
// }
|
||||
// } catch {}
|
||||
// })();
|
||||
// }, []);
|
||||
|
||||
if (!setupDone) {
|
||||
return <SetupScreen onReady={handleSetupReady} />;
|
||||
@@ -167,6 +166,7 @@ export default function App() {
|
||||
<Route path="dashboard" element={<DashboardPage />} />
|
||||
<Route path="settings" element={<SettingsPage />} />
|
||||
<Route path="get-started" element={<GetStartedPage />} />
|
||||
<Route path="data-sources" element={<DataSourcesPage />} />
|
||||
<Route path="agents" element={<AgentsPage />} />
|
||||
<Route path="logs" element={<LogsPage />} />
|
||||
</Route>
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { useRef, useEffect } from 'react';
|
||||
import { useRef, useEffect, useState, useCallback } from 'react';
|
||||
import { useNavigate } from 'react-router';
|
||||
import { MessageBubble } from './MessageBubble';
|
||||
import { InputArea } from './InputArea';
|
||||
import { StreamingDots } from './StreamingDots';
|
||||
import { useAppStore } from '../../lib/store';
|
||||
import { Sparkles, PanelRightOpen, PanelRightClose } from 'lucide-react';
|
||||
import { Sparkles, PanelRightOpen, PanelRightClose, Database, MessageSquare, X } from 'lucide-react';
|
||||
import { listConnectors } from '../../lib/connectors-api';
|
||||
|
||||
function getGreeting(): string {
|
||||
const hour = new Date().getHours();
|
||||
@@ -17,9 +19,20 @@ export function ChatArea() {
|
||||
const streamState = useAppStore((s) => s.streamState);
|
||||
const systemPanelOpen = useAppStore((s) => s.systemPanelOpen);
|
||||
const toggleSystemPanel = useAppStore((s) => s.toggleSystemPanel);
|
||||
const navigate = useNavigate();
|
||||
const listRef = useRef<HTMLDivElement>(null);
|
||||
const shouldAutoScroll = useRef(true);
|
||||
|
||||
// Check if any data sources are connected
|
||||
const [hasConnectedSources, setHasConnectedSources] = useState<boolean | null>(null);
|
||||
const [bannerDismissed, setBannerDismissed] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
listConnectors()
|
||||
.then((list) => setHasConnectedSources(list.some((c) => c.connected)))
|
||||
.catch(() => setHasConnectedSources(null));
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (shouldAutoScroll.current && listRef.current) {
|
||||
listRef.current.scrollTop = listRef.current.scrollHeight;
|
||||
@@ -49,6 +62,36 @@ export function ChatArea() {
|
||||
<PanelIcon size={16} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Data sources banner */}
|
||||
{hasConnectedSources === false && !bannerDismissed && (
|
||||
<div
|
||||
className="mx-4 mb-2 flex items-center gap-3 px-4 py-3 rounded-lg text-sm shrink-0"
|
||||
style={{
|
||||
background: 'var(--color-accent-subtle)',
|
||||
border: '1px solid var(--color-border)',
|
||||
}}
|
||||
>
|
||||
<Database size={16} style={{ color: 'var(--color-accent)', flexShrink: 0 }} />
|
||||
<span style={{ color: 'var(--color-text-secondary)', flex: 1 }}>
|
||||
Connect your data sources (Gmail, iMessage, Slack, etc.) to get personalized answers.
|
||||
</span>
|
||||
<button
|
||||
onClick={() => navigate('/data-sources')}
|
||||
className="px-3 py-1 rounded text-xs font-medium cursor-pointer"
|
||||
style={{ background: 'var(--color-accent)', color: '#fff', border: 'none' }}
|
||||
>
|
||||
Connect
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setBannerDismissed(true)}
|
||||
className="p-1 rounded cursor-pointer"
|
||||
style={{ color: 'var(--color-text-tertiary)', background: 'transparent', border: 'none' }}
|
||||
>
|
||||
<X size={14} />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
ref={listRef}
|
||||
onScroll={handleScroll}
|
||||
@@ -65,9 +108,41 @@ export function ChatArea() {
|
||||
<h2 className="text-xl font-semibold mb-2" style={{ color: 'var(--color-text)' }}>
|
||||
{getGreeting()}
|
||||
</h2>
|
||||
<p className="text-sm text-center max-w-sm" style={{ color: 'var(--color-text-secondary)' }}>
|
||||
<p className="text-sm text-center max-w-sm mb-6" style={{ color: 'var(--color-text-secondary)' }}>
|
||||
Ask anything. Your AI runs locally — private, fast, and always available.
|
||||
</p>
|
||||
|
||||
{/* Quick action hints */}
|
||||
<div className="flex gap-3">
|
||||
<button
|
||||
onClick={() => navigate('/data-sources')}
|
||||
className="flex items-center gap-2 px-4 py-2.5 rounded-lg text-xs cursor-pointer transition-colors"
|
||||
style={{
|
||||
background: 'var(--color-bg-secondary)',
|
||||
border: '1px solid var(--color-border)',
|
||||
color: 'var(--color-text-secondary)',
|
||||
}}
|
||||
onMouseEnter={(e) => (e.currentTarget.style.borderColor = 'var(--color-accent)')}
|
||||
onMouseLeave={(e) => (e.currentTarget.style.borderColor = 'var(--color-border)')}
|
||||
>
|
||||
<Database size={14} style={{ color: 'var(--color-accent)' }} />
|
||||
Connect Data Sources
|
||||
</button>
|
||||
<button
|
||||
onClick={() => { navigate('/data-sources'); setTimeout(() => window.dispatchEvent(new CustomEvent('switch-tab', { detail: 'messaging' })), 100); }}
|
||||
className="flex items-center gap-2 px-4 py-2.5 rounded-lg text-xs cursor-pointer transition-colors"
|
||||
style={{
|
||||
background: 'var(--color-bg-secondary)',
|
||||
border: '1px solid var(--color-border)',
|
||||
color: 'var(--color-text-secondary)',
|
||||
}}
|
||||
onMouseEnter={(e) => (e.currentTarget.style.borderColor = 'var(--color-accent)')}
|
||||
onMouseLeave={(e) => (e.currentTarget.style.borderColor = 'var(--color-border)')}
|
||||
>
|
||||
<MessageSquare size={14} style={{ color: 'var(--color-accent)' }} />
|
||||
Set Up Messaging Channels
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="max-w-[var(--chat-max-width)] mx-auto px-4 py-6">
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { Loader2, CheckCircle2, XCircle, Cpu, Server, Database } from 'lucide-react';
|
||||
import { getSetupStatus, type SetupStatus } from '../lib/api';
|
||||
import { SetupWizard } from './setup/SetupWizard';
|
||||
|
||||
const STEPS = [
|
||||
{ key: 'ollama_ready', label: 'Inference Engine', icon: Cpu, detail: 'Starting Ollama...' },
|
||||
@@ -71,15 +70,13 @@ function StepRow({
|
||||
|
||||
export function SetupScreen({ onReady }: { onReady: () => void }) {
|
||||
const [status, setStatus] = useState<SetupStatus | null>(null);
|
||||
const [showWizard, setShowWizard] = useState(false);
|
||||
|
||||
const poll = useCallback(async () => {
|
||||
const s = await getSetupStatus();
|
||||
if (s) setStatus(s);
|
||||
if (s?.phase === 'ready') {
|
||||
setTimeout(() => setShowWizard(true), 600);
|
||||
setTimeout(() => onReady(), 600);
|
||||
}
|
||||
}, []);
|
||||
}, [onReady]);
|
||||
|
||||
useEffect(() => {
|
||||
poll();
|
||||
@@ -87,10 +84,6 @@ export function SetupScreen({ onReady }: { onReady: () => void }) {
|
||||
return () => clearInterval(interval);
|
||||
}, [poll]);
|
||||
|
||||
if (showWizard) {
|
||||
return <SetupWizard onComplete={onReady} />;
|
||||
}
|
||||
|
||||
const activeStep: StepKey | null =
|
||||
status && !status.ollama_ready
|
||||
? 'ollama_ready'
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
Monitor,
|
||||
Loader2,
|
||||
ScrollText,
|
||||
Database,
|
||||
} from 'lucide-react';
|
||||
import { ConversationList } from './ConversationList';
|
||||
import { useAppStore } from '../../lib/store';
|
||||
@@ -53,6 +54,7 @@ export function Sidebar() {
|
||||
const navItems = [
|
||||
{ path: '/', icon: MessageSquare, label: 'Chat' },
|
||||
{ path: '/dashboard', icon: BarChart3, label: 'Dashboard' },
|
||||
{ path: '/data-sources', icon: Database, label: 'Data Sources' },
|
||||
{ path: '/agents', icon: Bot, label: 'Agents' },
|
||||
{ path: '/logs', icon: ScrollText, label: 'Logs' },
|
||||
{ path: '/settings', icon: Settings, label: 'Settings' },
|
||||
|
||||
@@ -31,7 +31,7 @@ export async function initApiBase(): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
const DESKTOP_API_FALLBACK = 'http://127.0.0.1:8222';
|
||||
const DESKTOP_API_FALLBACK = 'http://127.0.0.1:8000';
|
||||
|
||||
const getSettingsApiUrl = (): string => {
|
||||
try {
|
||||
|
||||
@@ -40,3 +40,14 @@ export async function getSyncStatus(id: string): Promise<SyncStatus> {
|
||||
if (!res.ok) throw new Error(`Failed to get sync status for ${id}: ${res.status}`);
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function triggerSync(id: string): Promise<{ connector_id: string; chunks_indexed: number; status: string }> {
|
||||
const res = await fetch(`${getBase()}/v1/connectors/${encodeURIComponent(id)}/sync`, {
|
||||
method: 'POST',
|
||||
});
|
||||
if (!res.ok) {
|
||||
const err = await res.json().catch(() => ({ detail: res.statusText }));
|
||||
throw new Error(err.detail || `Sync failed: ${res.status}`);
|
||||
}
|
||||
return res.json();
|
||||
}
|
||||
|
||||
@@ -1441,7 +1441,7 @@ function ChannelsTab({ agentId }: { agentId: string }) {
|
||||
color: 'var(--color-text-secondary)',
|
||||
fontSize: 12, marginBottom: 12,
|
||||
}}>
|
||||
Data sources your agent can search
|
||||
Data sources your agent can search across
|
||||
</div>
|
||||
|
||||
{/* Connected sources grid */}
|
||||
@@ -1771,13 +1771,14 @@ const MESSAGING_CHANNELS: MessagingChannelConfig[] = [
|
||||
icon: '#',
|
||||
description: 'DM your agent in any Slack workspace',
|
||||
setupSteps: [
|
||||
'1. Go to api.slack.com/apps \u2192 Create New App \u2192 From Scratch',
|
||||
'2. Go to App Manifest and paste this JSON to configure everything at once:',
|
||||
'{"display_information":{"name":"OpenJarvis"},"features":{"app_home":{"home_tab_enabled":true,"messages_tab_enabled":true,"messages_tab_read_only_enabled":false},"bot_user":{"display_name":"OpenJarvis","always_online":true}},"oauth_config":{"scopes":{"bot":["chat:write","im:write","im:read","im:history","users:read","channels:read","channels:history","app_mentions:read"]}},"settings":{"event_subscriptions":{"bot_events":["message.im"]},"socket_mode_enabled":true}}',
|
||||
'3. Save, then go to Install App \u2192 Install to Workspace \u2192 Authorize',
|
||||
'4. Copy the Bot User OAuth Token (xoxb-...) from OAuth & Permissions',
|
||||
'5. Go to Basic Information \u2192 App-Level Tokens \u2192 Generate Token \u2192 add connections:write scope \u2192 copy the token (xapp-...)',
|
||||
'6. Paste both tokens below',
|
||||
'1. Go to api.slack.com/apps → click "Create New App" → choose "From an app manifest"',
|
||||
'2. Select your workspace. When asked for the manifest format, choose JSON. Then paste the manifest below (click "Copy" to copy it):',
|
||||
'COPYABLE:{"display_information":{"name":"OpenJarvis"},"features":{"app_home":{"home_tab_enabled":true,"messages_tab_enabled":true,"messages_tab_read_only_enabled":false},"bot_user":{"display_name":"OpenJarvis","always_online":true}},"oauth_config":{"scopes":{"bot":["chat:write","im:write","im:read","im:history","mpim:read","mpim:history","users:read","channels:read","channels:history","channels:join","groups:read","groups:history","app_mentions:read"]}},"settings":{"event_subscriptions":{"bot_events":["message.im"]},"socket_mode_enabled":true}}',
|
||||
'3. Click "Next" → review the summary → click "Create". Then go to "Install App" in the left sidebar → click "Install to Workspace" → click "Allow"',
|
||||
'4. In the left sidebar, click "OAuth & Permissions". Copy the "Bot User OAuth Token" (starts with xoxb-...)',
|
||||
'5. In the left sidebar, click "Basic Information" → scroll to "App-Level Tokens" → click "Generate Token and Scopes" → name it "socket" → click "Add Scope" → select "connections:write" → click "Generate" → copy the token (starts with xapp-...)',
|
||||
'6. (Optional) Still in "Basic Information", scroll to "Display Information" → upload the OpenJarvis icon as the app icon',
|
||||
'7. Paste both tokens below and click Connect',
|
||||
],
|
||||
fields: [
|
||||
{ key: 'bot_token', label: 'Bot Token', placeholder: 'xoxb-...', type: 'password', required: true },
|
||||
@@ -1788,6 +1789,106 @@ const MESSAGING_CHANNELS: MessagingChannelConfig[] = [
|
||||
},
|
||||
];
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// SendBlue webhook step — ngrok tunnel + registration
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function SendBlueWebhookStep({
|
||||
apiKey, apiSecret, selectedNumber,
|
||||
}: {
|
||||
apiKey: string; apiSecret: string; selectedNumber: string;
|
||||
}) {
|
||||
const [webhookUrl, setWebhookUrl] = useState('');
|
||||
const [webhookStatus, setWebhookStatus] = useState<'idle' | 'registering' | 'done' | 'error'>('idle');
|
||||
|
||||
const registerWebhook = async () => {
|
||||
if (!webhookUrl.trim()) return;
|
||||
setWebhookStatus('registering');
|
||||
try {
|
||||
const url = webhookUrl.trim().replace(/\/+$/, '') + '/v1/channels/sendblue/webhook';
|
||||
await sendblueRegisterWebhook(apiKey, apiSecret, url);
|
||||
setWebhookStatus('done');
|
||||
} catch {
|
||||
setWebhookStatus('error');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ borderTop: '1px solid var(--color-border)', padding: 14, background: 'var(--color-bg)' }}>
|
||||
<div style={{
|
||||
background: '#052e16', border: '1px solid #2a5a3a',
|
||||
borderRadius: 6, padding: 12, marginBottom: 12, textAlign: 'center',
|
||||
}}>
|
||||
<div style={{ fontSize: 11, color: '#4ade80', fontWeight: 600, marginBottom: 4 }}>
|
||||
{'\u2713'} Your agent is now reachable via iMessage / SMS
|
||||
</div>
|
||||
<div style={{ fontSize: 18, fontWeight: 700, color: '#4ade80' }}>{selectedNumber}</div>
|
||||
</div>
|
||||
|
||||
{/* Webhook / ngrok step */}
|
||||
<div style={{ marginTop: 12 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 10 }}>
|
||||
<span style={{ background: '#7c3aed', color: 'white', borderRadius: '50%', width: 20, height: 20, display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 11, fontWeight: 700, flexShrink: 0 }}>4</span>
|
||||
<span style={{ fontSize: 12, fontWeight: 600 }}>Set up webhook to receive texts</span>
|
||||
</div>
|
||||
<div style={{
|
||||
fontSize: 11, lineHeight: 1.6,
|
||||
color: 'var(--color-text-secondary)',
|
||||
padding: '8px 10px', marginBottom: 10,
|
||||
background: 'var(--color-bg-secondary)',
|
||||
borderRadius: 6,
|
||||
borderLeft: '3px solid var(--color-accent, #7c3aed)',
|
||||
}}>
|
||||
<div><strong>1.</strong> Open a terminal and run: <code style={{ color: 'var(--color-accent)', background: 'var(--color-bg)', padding: '1px 4px', borderRadius: 3 }}>ngrok http 8000</code></div>
|
||||
<div style={{ marginTop: 4 }}><strong>2.</strong> Copy the <code style={{ color: 'var(--color-accent)', background: 'var(--color-bg)', padding: '1px 4px', borderRadius: 3 }}>https://</code> forwarding URL</div>
|
||||
<div style={{ marginTop: 4 }}><strong>3.</strong> Paste it below and click "Register Webhook"</div>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 6 }}>
|
||||
<input
|
||||
value={webhookUrl}
|
||||
onChange={(e) => { setWebhookUrl(e.target.value); setWebhookStatus('idle'); }}
|
||||
placeholder="https://abc123.ngrok-free.app"
|
||||
style={{
|
||||
flex: 1, padding: '7px 10px', background: 'var(--color-bg-secondary)',
|
||||
border: '1px solid var(--color-border)', borderRadius: 4,
|
||||
color: 'var(--color-text)', fontSize: 12, boxSizing: 'border-box' as const,
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
onClick={registerWebhook}
|
||||
disabled={!webhookUrl.trim() || webhookStatus === 'registering'}
|
||||
style={{
|
||||
fontSize: 11, padding: '7px 14px', whiteSpace: 'nowrap' as const,
|
||||
background: webhookStatus === 'done' ? '#22c55e' : '#7c3aed',
|
||||
color: 'white', border: 'none', borderRadius: 5,
|
||||
cursor: 'pointer', fontWeight: 600,
|
||||
opacity: !webhookUrl.trim() || webhookStatus === 'registering' ? 0.5 : 1,
|
||||
}}
|
||||
>
|
||||
{webhookStatus === 'registering' ? 'Registering...'
|
||||
: webhookStatus === 'done' ? 'Registered!'
|
||||
: webhookStatus === 'error' ? 'Retry'
|
||||
: 'Register Webhook'}
|
||||
</button>
|
||||
</div>
|
||||
{webhookStatus === 'done' && (
|
||||
<div style={{ fontSize: 11, color: '#22c55e', marginTop: 6 }}>
|
||||
Webhook registered! Incoming texts will be forwarded to your agent.
|
||||
</div>
|
||||
)}
|
||||
{webhookStatus === 'error' && (
|
||||
<div style={{ fontSize: 11, color: '#ef4444', marginTop: 6 }}>
|
||||
Failed to register. Check your ngrok URL and try again.
|
||||
</div>
|
||||
)}
|
||||
<div style={{ fontSize: 10, color: 'var(--color-text-tertiary)', marginTop: 8 }}>
|
||||
Don't have ngrok? <a href="https://ngrok.com/download" target="_blank" rel="noopener noreferrer" style={{ color: '#60a5fa', textDecoration: 'underline' }}>Download it free</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// SendBlue setup wizard — guided multi-step flow
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -2051,7 +2152,11 @@ function SendBlueWizard({
|
||||
<span style={{ fontSize: 12, fontWeight: 600 }}>Paste your API credentials</span>
|
||||
</div>
|
||||
<div style={{ fontSize: 11, color: 'var(--color-text-secondary)', marginBottom: 8 }}>
|
||||
Find these in your SendBlue dashboard under API settings.
|
||||
Go to your{' '}
|
||||
<a href="https://dashboard.sendblue.co/api-credentials" target="_blank" rel="noopener noreferrer" style={{ color: '#60a5fa', textDecoration: 'underline' }}>
|
||||
SendBlue API Credentials page
|
||||
</a>{' '}
|
||||
and copy the API Key and API Secret.
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: 8 }}>
|
||||
@@ -2152,22 +2257,13 @@ function SendBlueWizard({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Step 3: Done — success */}
|
||||
{/* Step 3: Done — success + webhook setup */}
|
||||
{step === 'done' && (
|
||||
<div style={{ borderTop: '1px solid var(--color-border)', padding: 14, background: 'var(--color-bg)' }}>
|
||||
<div style={{
|
||||
background: '#052e16', border: '1px solid #2a5a3a',
|
||||
borderRadius: 6, padding: 12, marginBottom: 12, textAlign: 'center',
|
||||
}}>
|
||||
<div style={{ fontSize: 11, color: '#4ade80', fontWeight: 600, marginBottom: 4 }}>
|
||||
{'\u2713'} Your agent is now reachable via iMessage / SMS
|
||||
</div>
|
||||
<div style={{ fontSize: 18, fontWeight: 700, color: '#4ade80' }}>{selectedNumber}</div>
|
||||
</div>
|
||||
<div style={{ fontSize: 11, color: 'var(--color-text-secondary)', lineHeight: 1.5 }}>
|
||||
Text this number from any phone. Your agent will research your personal data and respond via iMessage (blue bubbles) when possible.
|
||||
</div>
|
||||
</div>
|
||||
<SendBlueWebhookStep
|
||||
apiKey={apiKey}
|
||||
apiSecret={apiSecret}
|
||||
selectedNumber={selectedNumber}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
@@ -2231,7 +2327,7 @@ function MessagingTab({ agentId }: { agentId: string }) {
|
||||
color: 'var(--color-text-secondary)',
|
||||
fontSize: 12, marginBottom: 14,
|
||||
}}>
|
||||
Connect a messaging platform so you can talk to your agent from your phone or other devices.
|
||||
Connect a messaging channel so you can talk to your agent from your phone or other devices.
|
||||
</div>
|
||||
|
||||
{/* SendBlue wizard — primary option */}
|
||||
@@ -2248,7 +2344,7 @@ function MessagingTab({ agentId }: { agentId: string }) {
|
||||
textTransform: 'uppercase', letterSpacing: 1,
|
||||
margin: '14px 0 8px', fontWeight: 600,
|
||||
}}>
|
||||
Other channels
|
||||
Other messaging channels
|
||||
</div>
|
||||
|
||||
{MESSAGING_CHANNELS.map((ch) => {
|
||||
@@ -2358,11 +2454,41 @@ function MessagingTab({ agentId }: { agentId: string }) {
|
||||
borderRadius: 6,
|
||||
borderLeft: '3px solid var(--color-accent, #7c3aed)',
|
||||
}}>
|
||||
{ch.setupSteps.map((step, i) => (
|
||||
<div key={i} style={{ marginBottom: i < ch.setupSteps.length - 1 ? 4 : 0 }}>
|
||||
{step}
|
||||
</div>
|
||||
))}
|
||||
{ch.setupSteps.map((step, i) => {
|
||||
if (step.startsWith('COPYABLE:')) {
|
||||
const text = step.slice(9);
|
||||
return (
|
||||
<div key={i} style={{ marginBottom: 6, marginTop: 4 }}>
|
||||
<div style={{
|
||||
position: 'relative',
|
||||
background: 'var(--color-bg)',
|
||||
border: '1px solid var(--color-border)',
|
||||
borderRadius: 4, padding: '8px 10px',
|
||||
fontSize: 10, fontFamily: 'monospace',
|
||||
wordBreak: 'break-all', lineHeight: 1.4,
|
||||
maxHeight: 80, overflowY: 'auto',
|
||||
}}>
|
||||
{text}
|
||||
<button
|
||||
onClick={() => { navigator.clipboard.writeText(text); }}
|
||||
style={{
|
||||
position: 'sticky', float: 'right', top: 0,
|
||||
fontSize: 10, padding: '2px 8px',
|
||||
background: '#7c3aed', color: 'white',
|
||||
border: 'none', borderRadius: 3,
|
||||
cursor: 'pointer', fontWeight: 600,
|
||||
}}
|
||||
>Copy</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div key={i} style={{ marginBottom: i < ch.setupSteps.length - 1 ? 4 : 0 }}>
|
||||
{step}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Form fields */}
|
||||
@@ -2822,8 +2948,8 @@ export function AgentsPage() {
|
||||
const DETAIL_TABS = [
|
||||
{ id: 'overview', label: 'Overview', icon: Activity },
|
||||
{ id: 'interact', label: 'Interact', icon: MessageSquare },
|
||||
{ id: 'channels', label: 'Channels', icon: Database },
|
||||
{ id: 'messaging', label: 'Messaging', icon: Wifi },
|
||||
{ id: 'channels', label: 'Data Sources', icon: Database },
|
||||
{ id: 'messaging', label: 'Messaging Channels', icon: Wifi },
|
||||
{ id: 'tasks', label: 'Tasks', icon: ListTodo },
|
||||
{ id: 'memory', label: 'Memory', icon: Brain },
|
||||
{ id: 'learning', label: 'Learning', icon: Settings },
|
||||
@@ -2946,6 +3072,34 @@ export function AgentsPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Hint for deep research agents */}
|
||||
{selectedAgent.agent_type === 'deep_research' && (
|
||||
<div
|
||||
className="flex items-start gap-3 p-3 rounded-lg text-sm"
|
||||
style={{
|
||||
background: 'var(--color-accent-subtle)',
|
||||
border: '1px solid var(--color-border)',
|
||||
}}
|
||||
>
|
||||
<Database size={16} style={{ color: 'var(--color-accent)', flexShrink: 0, marginTop: 2 }} />
|
||||
<div style={{ color: 'var(--color-text-secondary)' }}>
|
||||
<strong>Tip:</strong> Connect your personal data in the{' '}
|
||||
<button
|
||||
onClick={() => setDetailTab('channels')}
|
||||
className="cursor-pointer underline"
|
||||
style={{ color: 'var(--color-accent)', background: 'none', border: 'none', padding: 0, font: 'inherit' }}
|
||||
>Data Sources</button>{' '}
|
||||
tab, then set up{' '}
|
||||
<button
|
||||
onClick={() => setDetailTab('messaging')}
|
||||
className="cursor-pointer underline"
|
||||
style={{ color: 'var(--color-accent)', background: 'none', border: 'none', padding: 0, font: 'inherit' }}
|
||||
>Messaging Channels</button>{' '}
|
||||
to talk to this agent from your phone.
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Usage stats + savings — single compact row */}
|
||||
{(() => {
|
||||
const inTok = selectedAgent.input_tokens ?? 0;
|
||||
@@ -3029,7 +3183,7 @@ export function AgentsPage() {
|
||||
style={{ background: 'var(--color-bg-secondary)', border: '1px solid var(--color-border)' }}
|
||||
>
|
||||
<h3 className="text-sm font-medium mb-2" style={{ color: 'var(--color-text-secondary)' }}>
|
||||
Channel Bindings
|
||||
Messaging Channels
|
||||
</h3>
|
||||
{channels.map((b) => (
|
||||
<div key={b.id} className="text-sm py-1" style={{ color: 'var(--color-text)' }}>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -53,8 +53,9 @@ export type WizardStep = "pick" | "connect" | "ingest" | "ready";
|
||||
export type SourceCard = ConnectorMeta;
|
||||
|
||||
export const SOURCE_CATALOG: ConnectorMeta[] = [
|
||||
// ── Communication ──────────────────────────────────────────────────
|
||||
{
|
||||
connector_id: 'gmail',
|
||||
connector_id: 'gmail_imap',
|
||||
display_name: 'Gmail',
|
||||
auth_type: 'oauth',
|
||||
category: 'communication',
|
||||
@@ -64,19 +65,19 @@ export const SOURCE_CATALOG: ConnectorMeta[] = [
|
||||
unitLabel: 'emails',
|
||||
steps: [
|
||||
{
|
||||
label: 'Enable 2-Factor Authentication on your Google account',
|
||||
label: 'Go to your Google account security settings and make sure 2-Step Verification is turned ON (required for app passwords)',
|
||||
url: 'https://myaccount.google.com/signinoptions/two-step-verification',
|
||||
urlLabel: 'Open Google Security',
|
||||
},
|
||||
{
|
||||
label: 'Generate an App Password for "Mail"',
|
||||
label: 'Go to App Passwords (myaccount.google.com/apppasswords). Select app: "Mail", select device: "Other" and type "OpenJarvis". Click Generate. Google will show a 16-character password — copy it now (you won\'t see it again)',
|
||||
url: 'https://myaccount.google.com/apppasswords',
|
||||
urlLabel: 'Open App Passwords',
|
||||
},
|
||||
{ label: 'Paste your credentials below' },
|
||||
{ label: 'Enter your Gmail address and the 16-character app password below (spaces are fine)' },
|
||||
],
|
||||
inputFields: [
|
||||
{ name: 'email', placeholder: 'Email address', type: 'text' },
|
||||
{ name: 'email', placeholder: 'you@gmail.com', type: 'text' },
|
||||
{ name: 'password', placeholder: 'App password (xxxx xxxx xxxx xxxx)', type: 'password' },
|
||||
],
|
||||
},
|
||||
@@ -87,15 +88,31 @@ export const SOURCE_CATALOG: ConnectorMeta[] = [
|
||||
category: 'communication',
|
||||
icon: 'Hash',
|
||||
color: 'text-purple-400',
|
||||
description: 'Channel messages and threads',
|
||||
description: 'Read messages from channels, DMs, and threads',
|
||||
unitLabel: 'messages',
|
||||
steps: [
|
||||
{
|
||||
label: 'Go to your Slack App settings and copy the Bot User OAuth Token',
|
||||
label: 'Go to api.slack.com/apps and click "Create New App" → choose "From scratch". Name it "OpenJarvis" and pick your workspace',
|
||||
url: 'https://api.slack.com/apps',
|
||||
urlLabel: 'Open Slack Apps',
|
||||
},
|
||||
{ label: 'Paste the bot token below (starts with xoxb-)' },
|
||||
{
|
||||
label: 'In the left sidebar, click "OAuth & Permissions". Scroll down to "Bot Token Scopes" and click "Add an OAuth Scope" to add EACH of these scopes one by one:',
|
||||
},
|
||||
{
|
||||
label: 'channels:read • channels:history • channels:join • groups:read • groups:history • im:read • im:history • mpim:read • mpim:history • chat:write • users:read • app_mentions:read',
|
||||
},
|
||||
{
|
||||
label: 'In the left sidebar, click "Install App" → click "Install to Workspace" → click "Allow". After installing, copy the "Bot User OAuth Token" that appears (starts with xoxb-)',
|
||||
},
|
||||
{
|
||||
label: 'Paste the bot token below. After connecting, invite the bot to channels you want indexed by typing /invite @OpenJarvis in each channel',
|
||||
},
|
||||
{
|
||||
label: '(Optional) Set the app icon: in the left sidebar click "Basic Information" → scroll to "Display Information" → upload the OpenJarvis logo',
|
||||
url: 'https://github.com/open-jarvis/OpenJarvis/blob/main/assets/openjarvis-slack-icon.jpg',
|
||||
urlLabel: 'Download icon',
|
||||
},
|
||||
],
|
||||
inputFields: [
|
||||
{ name: 'token', placeholder: 'xoxb-...', type: 'password' },
|
||||
@@ -112,12 +129,19 @@ export const SOURCE_CATALOG: ConnectorMeta[] = [
|
||||
unitLabel: 'pages',
|
||||
steps: [
|
||||
{
|
||||
label: 'Create an internal integration and copy the secret',
|
||||
label: 'Go to notion.so/profile/integrations → click "+ New integration". Name it "OpenJarvis", select your workspace, and click Submit',
|
||||
url: 'https://www.notion.so/profile/integrations',
|
||||
urlLabel: 'Open Notion Integrations',
|
||||
},
|
||||
{ label: 'Paste the integration token below (starts with ntn_)' },
|
||||
{ label: 'Then share pages with your integration: Page → ... → Connections → Add' },
|
||||
{
|
||||
label: 'Copy the "Internal Integration Secret" (starts with ntn_) and paste it below',
|
||||
},
|
||||
{
|
||||
label: 'To share ALL your pages at once: open any top-level page → click "..." (top right) → "Connections" → "Add connections" → search "OpenJarvis" → click it. This shares the page and all its sub-pages. Repeat for each top-level page, or share your entire workspace by doing this on every root page',
|
||||
},
|
||||
{
|
||||
label: 'Tip: if you have a single top-level page that contains everything, sharing just that one page will share all nested sub-pages automatically',
|
||||
},
|
||||
],
|
||||
inputFields: [
|
||||
{ name: 'token', placeholder: 'ntn_...', type: 'password' },
|
||||
@@ -133,8 +157,8 @@ export const SOURCE_CATALOG: ConnectorMeta[] = [
|
||||
description: 'AI meeting notes',
|
||||
unitLabel: 'meeting notes',
|
||||
steps: [
|
||||
{ label: 'Open the Granola desktop app → Settings → API' },
|
||||
{ label: 'Copy your API key and paste below' },
|
||||
{ label: 'Open the Granola desktop app. Click the gear icon (Settings) in the bottom-left corner, then click "API"' },
|
||||
{ label: 'Click "Generate API Key" (or copy your existing key). Paste the key below' },
|
||||
],
|
||||
inputFields: [
|
||||
{ name: 'token', placeholder: 'grn_...', type: 'password' },
|
||||
@@ -151,18 +175,17 @@ export const SOURCE_CATALOG: ConnectorMeta[] = [
|
||||
unitLabel: 'messages',
|
||||
steps: [
|
||||
{
|
||||
label: 'Open System Settings → Privacy & Security → Full Disk Access',
|
||||
url: 'x-apple.systempreferences:com.apple.preference.security?Privacy_AllFiles',
|
||||
urlLabel: 'Open System Settings',
|
||||
label: 'Open the Apple menu () → System Settings → Privacy & Security (in the left sidebar) → scroll down and click "Full Disk Access"',
|
||||
},
|
||||
{
|
||||
label: 'Enable Full Disk Access for your terminal app (Terminal, iTerm, or Warp)',
|
||||
label: 'Click the "+" button at the bottom of the list. Navigate to Applications → Utilities → select "Terminal.app" (or iTerm2/Warp if you use those). If you\'re using the desktop app, also add "OpenJarvis.app" from Applications',
|
||||
},
|
||||
{
|
||||
label: 'iMessage history will be detected automatically once access is granted',
|
||||
label: 'Toggle the switch ON next to each app you added. Close and reopen your terminal (or restart OpenJarvis). iMessage data will be detected automatically — no credentials needed',
|
||||
},
|
||||
],
|
||||
},
|
||||
// ── Documents ──────────────────────────────────────────────────────
|
||||
{
|
||||
connector_id: 'obsidian',
|
||||
display_name: 'Obsidian',
|
||||
@@ -174,10 +197,13 @@ export const SOURCE_CATALOG: ConnectorMeta[] = [
|
||||
unitLabel: 'notes',
|
||||
steps: [
|
||||
{
|
||||
label: 'Find your Obsidian vault folder on your computer. It\'s the folder containing the .obsidian directory.',
|
||||
label: 'Find your vault path: open Obsidian → click the vault name in the bottom-left corner → "Manage Vaults" → look at the path shown under your vault name. On macOS this is usually ~/Documents/MyVault or ~/Library/Mobile Documents/iCloud~md~obsidian/Documents/MyVault',
|
||||
},
|
||||
{
|
||||
label: 'Paste the full path below (e.g. /Users/you/Documents/MyVault)',
|
||||
label: 'Alternatively, open Finder → navigate to your vault folder (it contains a hidden .obsidian directory). Right-click the folder → "Copy as Pathname" to get the full path',
|
||||
},
|
||||
{
|
||||
label: 'Paste the full path below. OpenJarvis will index all .md files in the vault',
|
||||
},
|
||||
],
|
||||
inputFields: [
|
||||
@@ -195,22 +221,22 @@ export const SOURCE_CATALOG: ConnectorMeta[] = [
|
||||
unitLabel: 'files',
|
||||
steps: [
|
||||
{
|
||||
label: 'Go to the Google Cloud Console and create a project (or use an existing one)',
|
||||
label: 'Go to Google Cloud Console → create a new project (or select an existing one). Give it any name (e.g. "OpenJarvis")',
|
||||
url: 'https://console.cloud.google.com/projectcreate',
|
||||
urlLabel: 'Open Google Cloud Console',
|
||||
urlLabel: 'Create Project',
|
||||
},
|
||||
{
|
||||
label: 'Enable the Google Drive API for your project',
|
||||
label: 'Enable the Google Drive API: click the link below, make sure your project is selected at the top, then click "Enable"',
|
||||
url: 'https://console.cloud.google.com/apis/library/drive.googleapis.com',
|
||||
urlLabel: 'Enable Drive API',
|
||||
},
|
||||
{
|
||||
label: 'Go to Credentials → Create OAuth 2.0 Client ID (choose "Desktop app")',
|
||||
label: 'Create OAuth credentials: go to Credentials (link below) → click "+ Create Credentials" → choose "OAuth client ID" → Application type: "Desktop app" → click "Create"',
|
||||
url: 'https://console.cloud.google.com/apis/credentials',
|
||||
urlLabel: 'Open Credentials',
|
||||
},
|
||||
{
|
||||
label: 'Copy the Client ID and Client Secret from your OAuth client, then paste each one below',
|
||||
label: 'A dialog will show your Client ID and Client Secret. Copy both and paste them below. (If you miss it, click the download icon next to your OAuth client to see them again)',
|
||||
},
|
||||
],
|
||||
inputFields: [
|
||||
@@ -218,9 +244,10 @@ export const SOURCE_CATALOG: ConnectorMeta[] = [
|
||||
{ name: 'password', placeholder: 'Client Secret', type: 'password' },
|
||||
],
|
||||
},
|
||||
// ── PIM (Calendar, Contacts) ───────────────────────────────────────
|
||||
{
|
||||
connector_id: 'gcalendar',
|
||||
display_name: 'Calendar',
|
||||
display_name: 'Google Calendar',
|
||||
auth_type: 'oauth',
|
||||
category: 'pim',
|
||||
icon: 'Calendar',
|
||||
@@ -229,32 +256,32 @@ export const SOURCE_CATALOG: ConnectorMeta[] = [
|
||||
unitLabel: 'events',
|
||||
steps: [
|
||||
{
|
||||
label: 'Go to the Google Cloud Console and create a project (or use an existing one)',
|
||||
label: 'Go to Google Cloud Console → use the same project as Google Drive (or create a new one)',
|
||||
url: 'https://console.cloud.google.com/projectcreate',
|
||||
urlLabel: 'Open Google Cloud Console',
|
||||
urlLabel: 'Open Console',
|
||||
},
|
||||
{
|
||||
label: 'Enable the Google Calendar API for your project',
|
||||
label: 'Enable the Google Calendar API: click the link below, select your project, then click "Enable"',
|
||||
url: 'https://console.cloud.google.com/apis/library/calendar-json.googleapis.com',
|
||||
urlLabel: 'Enable Calendar API',
|
||||
},
|
||||
{
|
||||
label: 'Go to Credentials → Create OAuth 2.0 Client ID (choose "Desktop app")',
|
||||
label: 'Go to Credentials → "+ Create Credentials" → "OAuth client ID" → Application type: "Desktop app" → "Create". Copy the Client ID and Client Secret',
|
||||
url: 'https://console.cloud.google.com/apis/credentials',
|
||||
urlLabel: 'Open Credentials',
|
||||
},
|
||||
{
|
||||
label: 'Copy the Client ID and Client Secret from your OAuth client, then paste each one below',
|
||||
label: 'Paste the Client ID and Client Secret below (you can reuse the same OAuth client as Google Drive if you enabled both APIs in the same project)',
|
||||
},
|
||||
],
|
||||
inputFields: [
|
||||
{ name: 'email', placeholder: 'Client ID (e.g. 123456-abc.apps.googleusercontent.com)', type: 'text' },
|
||||
{ name: 'email', placeholder: 'Client ID', type: 'text' },
|
||||
{ name: 'password', placeholder: 'Client Secret', type: 'password' },
|
||||
],
|
||||
},
|
||||
{
|
||||
connector_id: 'gcontacts',
|
||||
display_name: 'Contacts',
|
||||
display_name: 'Google Contacts',
|
||||
auth_type: 'oauth',
|
||||
category: 'pim',
|
||||
icon: 'Users',
|
||||
@@ -263,26 +290,26 @@ export const SOURCE_CATALOG: ConnectorMeta[] = [
|
||||
unitLabel: 'contacts',
|
||||
steps: [
|
||||
{
|
||||
label: 'Go to the Google Cloud Console and create a project (or use an existing one)',
|
||||
label: 'Go to Google Cloud Console → use the same project as Google Drive (or create a new one)',
|
||||
url: 'https://console.cloud.google.com/projectcreate',
|
||||
urlLabel: 'Open Google Cloud Console',
|
||||
urlLabel: 'Open Console',
|
||||
},
|
||||
{
|
||||
label: 'Enable the People API for your project',
|
||||
label: 'Enable the People API: click the link below, select your project, then click "Enable"',
|
||||
url: 'https://console.cloud.google.com/apis/library/people.googleapis.com',
|
||||
urlLabel: 'Enable People API',
|
||||
},
|
||||
{
|
||||
label: 'Go to Credentials → Create OAuth 2.0 Client ID (choose "Desktop app")',
|
||||
label: 'Go to Credentials → "+ Create Credentials" → "OAuth client ID" → Application type: "Desktop app" → "Create". Copy the Client ID and Client Secret',
|
||||
url: 'https://console.cloud.google.com/apis/credentials',
|
||||
urlLabel: 'Open Credentials',
|
||||
},
|
||||
{
|
||||
label: 'Copy the Client ID and Client Secret from your OAuth client, then paste each one below',
|
||||
label: 'Paste the Client ID and Client Secret below',
|
||||
},
|
||||
],
|
||||
inputFields: [
|
||||
{ name: 'email', placeholder: 'Client ID (e.g. 123456-abc.apps.googleusercontent.com)', type: 'text' },
|
||||
{ name: 'email', placeholder: 'Client ID', type: 'text' },
|
||||
{ name: 'password', placeholder: 'Client Secret', type: 'password' },
|
||||
],
|
||||
},
|
||||
@@ -297,15 +324,13 @@ export const SOURCE_CATALOG: ConnectorMeta[] = [
|
||||
unitLabel: 'notes',
|
||||
steps: [
|
||||
{
|
||||
label: 'Open System Settings → Privacy & Security → Full Disk Access',
|
||||
url: 'x-apple.systempreferences:com.apple.preference.security?Privacy_AllFiles',
|
||||
urlLabel: 'Open System Settings',
|
||||
label: 'Open the Apple menu () → System Settings → Privacy & Security (in the left sidebar) → scroll down and click "Full Disk Access"',
|
||||
},
|
||||
{
|
||||
label: 'Enable Full Disk Access for your terminal app (Terminal, iTerm, or Warp)',
|
||||
label: 'Click the "+" button at the bottom of the list. Navigate to Applications → Utilities → select "Terminal.app" (or iTerm2/Warp if you use those). If you\'re using the desktop app, also add "OpenJarvis.app" from Applications',
|
||||
},
|
||||
{
|
||||
label: 'Apple Notes will be detected automatically once access is granted',
|
||||
label: 'Toggle the switch ON next to each app you added. Close and reopen your terminal (or restart OpenJarvis). Apple Notes will be detected automatically — no credentials needed',
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -320,20 +345,23 @@ export const SOURCE_CATALOG: ConnectorMeta[] = [
|
||||
unitLabel: 'emails',
|
||||
steps: [
|
||||
{
|
||||
label: 'Go to the Azure Portal and register a new application',
|
||||
label: 'Go to the Azure Portal → App Registrations → click "+ New registration". Name it "OpenJarvis", select "Accounts in this organizational directory only", and click Register',
|
||||
url: 'https://portal.azure.com/#view/Microsoft_AAD_RegisteredApps/ApplicationsListBlade',
|
||||
urlLabel: 'Open Azure App Registrations',
|
||||
},
|
||||
{
|
||||
label: 'Under API Permissions, add Microsoft Graph → Mail.Read',
|
||||
label: 'In the left sidebar, click "API Permissions" → "Add a permission" → "Microsoft Graph" → "Delegated permissions" → search and check "Mail.Read" → click "Add permissions"',
|
||||
},
|
||||
{
|
||||
label: 'Create a client secret and paste the Application (client) ID and secret below',
|
||||
label: 'In the left sidebar, click "Certificates & secrets" → "New client secret" → set a description and expiry → click "Add" → immediately copy the "Value" (you won\'t see it again)',
|
||||
},
|
||||
{
|
||||
label: 'Go to "Overview" in the left sidebar and copy the "Application (client) ID". Paste both the Client ID and the Client Secret below',
|
||||
},
|
||||
],
|
||||
inputFields: [
|
||||
{ name: 'email', placeholder: 'Client ID (e.g. 123456-abc.apps.googleusercontent.com)', type: 'text' },
|
||||
{ name: 'password', placeholder: 'Client Secret', type: 'password' },
|
||||
{ name: 'email', placeholder: 'Application (client) ID', type: 'text' },
|
||||
{ name: 'password', placeholder: 'Client Secret Value', type: 'password' },
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -347,18 +375,15 @@ export const SOURCE_CATALOG: ConnectorMeta[] = [
|
||||
unitLabel: 'files',
|
||||
steps: [
|
||||
{
|
||||
label: 'Go to the Dropbox App Console and create a new app',
|
||||
label: 'Go to the Dropbox App Console and click "Create app". Choose "Scoped access" → "Full Dropbox" → give it a name (e.g. "OpenJarvis") → click "Create app"',
|
||||
url: 'https://www.dropbox.com/developers/apps/create',
|
||||
urlLabel: 'Open Dropbox App Console',
|
||||
},
|
||||
{
|
||||
label: 'Choose "Scoped access" → "Full Dropbox" → name your app',
|
||||
label: 'Click the "Permissions" tab at the top. Check "files.metadata.read" and "files.content.read" → click "Submit" at the bottom to save',
|
||||
},
|
||||
{
|
||||
label: 'Under Permissions, enable "files.metadata.read" and "files.content.read"',
|
||||
},
|
||||
{
|
||||
label: 'Go to Settings → Generate an access token and paste it below',
|
||||
label: 'Go back to the "Settings" tab. Under "OAuth 2", find "Generated access token" and click "Generate". Copy the token and paste it below',
|
||||
},
|
||||
],
|
||||
inputFields: [
|
||||
@@ -372,19 +397,19 @@ export const SOURCE_CATALOG: ConnectorMeta[] = [
|
||||
category: 'communication',
|
||||
icon: 'MessageSquare',
|
||||
color: 'text-green-400',
|
||||
description: 'WhatsApp messages',
|
||||
description: 'WhatsApp messages (Meta Cloud API)',
|
||||
unitLabel: 'messages',
|
||||
steps: [
|
||||
{
|
||||
label: 'Go to Meta for Developers and create a WhatsApp Business app',
|
||||
label: 'Go to Meta for Developers → click "Create App" → choose "Business" type → fill in your app details and click "Create App"',
|
||||
url: 'https://developers.facebook.com/apps/',
|
||||
urlLabel: 'Open Meta Developer Portal',
|
||||
},
|
||||
{
|
||||
label: 'Set up WhatsApp → Get a temporary access token from the API Setup page',
|
||||
label: 'On the app dashboard, find "WhatsApp" and click "Set up". Follow the prompts to add a WhatsApp test number. Go to "API Setup" and copy the temporary access token',
|
||||
},
|
||||
{
|
||||
label: 'Copy your Phone Number ID and Access Token, paste below as "phone_id:token"',
|
||||
label: 'Copy your "Phone Number ID" (shown on the API Setup page) and the access token. Paste them below separated by a colon — e.g. 123456789:EAABx...',
|
||||
},
|
||||
],
|
||||
inputFields: [
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[template]
|
||||
id = "personal_deep_research"
|
||||
name = "Personal Deep Research"
|
||||
description = "Research your emails, messages, meetings, and docs. Answers questions with cited reports."
|
||||
description = "Searches your personal data and produces cited reports."
|
||||
agent_type = "deep_research"
|
||||
schedule_type = "manual"
|
||||
schedule_value = ""
|
||||
|
||||
@@ -48,6 +48,21 @@ try:
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
try:
|
||||
import openjarvis.connectors.slack_connector # noqa: F401
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
try:
|
||||
import openjarvis.connectors.outlook # noqa: F401
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
try:
|
||||
import openjarvis.connectors.gcalendar # noqa: F401
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
try:
|
||||
import openjarvis.connectors.dropbox # noqa: F401
|
||||
except ImportError:
|
||||
|
||||
@@ -92,7 +92,7 @@ class GmailIMAPConnector(BaseConnector):
|
||||
credentials_path: str = "",
|
||||
*,
|
||||
imap_host: str = "",
|
||||
max_messages: int = 500,
|
||||
max_messages: int = 5000,
|
||||
) -> None:
|
||||
self._email = email_address
|
||||
self._password = app_password
|
||||
|
||||
@@ -25,7 +25,7 @@ from openjarvis.tools._stubs import ToolSpec
|
||||
|
||||
_SLACK_API_BASE = "https://slack.com/api"
|
||||
_SLACK_AUTH_ENDPOINT = "https://slack.com/oauth/v2/authorize"
|
||||
_SLACK_SCOPES = "channels:history,channels:read,users:read"
|
||||
_SLACK_SCOPES = "channels:read,channels:history,groups:read,groups:history,im:read,im:history,mpim:read,mpim:history,users:read"
|
||||
_DEFAULT_CREDENTIALS_PATH = str(DEFAULT_CONFIG_DIR / "connectors" / "slack.json")
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -59,14 +59,7 @@ def _slack_api_conversations_list(
|
||||
if cursor:
|
||||
params["cursor"] = cursor
|
||||
|
||||
resp = httpx.get(
|
||||
f"{_SLACK_API_BASE}/conversations.list",
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
params=params,
|
||||
timeout=30.0,
|
||||
)
|
||||
resp.raise_for_status()
|
||||
return resp.json()
|
||||
return _slack_api_with_retry("conversations.list", token, params)
|
||||
|
||||
|
||||
def _slack_api_conversations_history(
|
||||
@@ -95,14 +88,7 @@ def _slack_api_conversations_history(
|
||||
if cursor:
|
||||
params["cursor"] = cursor
|
||||
|
||||
resp = httpx.get(
|
||||
f"{_SLACK_API_BASE}/conversations.history",
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
params=params,
|
||||
timeout=30.0,
|
||||
)
|
||||
resp.raise_for_status()
|
||||
return resp.json()
|
||||
return _slack_api_with_retry("conversations.history", token, params)
|
||||
|
||||
|
||||
def _slack_api_users_list(token: str) -> Dict[str, Any]:
|
||||
@@ -118,13 +104,47 @@ def _slack_api_users_list(token: str) -> Dict[str, Any]:
|
||||
dict
|
||||
Raw API response containing ``members`` list.
|
||||
"""
|
||||
resp = httpx.get(
|
||||
f"{_SLACK_API_BASE}/users.list",
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
timeout=30.0,
|
||||
)
|
||||
resp.raise_for_status()
|
||||
return resp.json()
|
||||
return _slack_api_with_retry("users.list", token)
|
||||
|
||||
|
||||
def _slack_api_with_retry(
|
||||
method: str,
|
||||
token: str,
|
||||
params: Optional[Dict[str, str]] = None,
|
||||
max_retries: int = 3,
|
||||
http_method: str = "GET",
|
||||
) -> Dict[str, Any]:
|
||||
"""Call a Slack API method with automatic retry on rate limits."""
|
||||
import time as _time
|
||||
|
||||
for attempt in range(max_retries + 1):
|
||||
if http_method == "POST":
|
||||
resp = httpx.post(
|
||||
f"{_SLACK_API_BASE}/{method}",
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
json=params or {},
|
||||
timeout=30.0,
|
||||
)
|
||||
else:
|
||||
resp = httpx.get(
|
||||
f"{_SLACK_API_BASE}/{method}",
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
params=params or {},
|
||||
timeout=30.0,
|
||||
)
|
||||
if resp.status_code == 429:
|
||||
retry_after = int(resp.headers.get("Retry-After", "5"))
|
||||
if attempt < max_retries:
|
||||
_time.sleep(retry_after)
|
||||
continue
|
||||
resp.raise_for_status()
|
||||
data = resp.json()
|
||||
if not data.get("ok") and data.get("error") == "ratelimited":
|
||||
if attempt < max_retries:
|
||||
_time.sleep(5)
|
||||
continue
|
||||
return data
|
||||
return {}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -267,15 +287,37 @@ class SlackConnector(BaseConnector):
|
||||
for channel in channels:
|
||||
chan_id: str = channel.get("id", "")
|
||||
chan_name: str = channel.get("name", chan_id)
|
||||
is_member: bool = channel.get("is_member", False)
|
||||
is_private: bool = channel.get("is_private", False)
|
||||
if not chan_id:
|
||||
continue
|
||||
|
||||
# Auto-join public channels; skip private channels the bot isn't in
|
||||
if not is_member:
|
||||
if is_private:
|
||||
continue # Can't join private channels without invite
|
||||
# Try to join the public channel
|
||||
try:
|
||||
join_resp = _slack_api_with_retry(
|
||||
"conversations.join", token, {"channel": chan_id},
|
||||
http_method="POST",
|
||||
)
|
||||
if not join_resp.get("ok"):
|
||||
continue
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
# Step 3: paginate through message history
|
||||
history_cursor = ""
|
||||
while True:
|
||||
history_resp = _slack_api_conversations_history(
|
||||
token, chan_id, cursor=history_cursor
|
||||
)
|
||||
try:
|
||||
history_resp = _slack_api_conversations_history(
|
||||
token, chan_id, cursor=history_cursor
|
||||
)
|
||||
except Exception:
|
||||
break # Skip channels we can't read
|
||||
if not history_resp.get("ok", True):
|
||||
break # not_in_channel or other error
|
||||
messages: List[Dict[str, Any]] = history_resp.get("messages", [])
|
||||
|
||||
for msg in messages:
|
||||
|
||||
@@ -282,9 +282,14 @@ def create_connectors_router():
|
||||
"status": "disconnected",
|
||||
}
|
||||
|
||||
# Track which connectors have a background sync running
|
||||
_sync_threads: Dict[str, Any] = {}
|
||||
|
||||
@router.post("/{connector_id}/sync")
|
||||
def trigger_sync(connector_id: str) -> Dict[str, Any]:
|
||||
"""Trigger an incremental sync for a connector."""
|
||||
"""Trigger a sync in the background and return immediately."""
|
||||
import threading
|
||||
|
||||
_ensure_connectors_registered()
|
||||
if not ConnectorRegistry.contains(connector_id):
|
||||
raise HTTPException(
|
||||
@@ -298,19 +303,49 @@ def create_connectors_router():
|
||||
detail=f"Connector '{connector_id}' is not connected",
|
||||
)
|
||||
|
||||
from openjarvis.connectors.pipeline import IngestionPipeline
|
||||
from openjarvis.connectors.store import KnowledgeStore
|
||||
from openjarvis.connectors.sync_engine import SyncEngine
|
||||
# If already syncing, don't start another
|
||||
existing = _sync_threads.get(connector_id)
|
||||
if existing and existing.is_alive():
|
||||
return {
|
||||
"connector_id": connector_id,
|
||||
"status": "already_syncing",
|
||||
}
|
||||
|
||||
store = KnowledgeStore()
|
||||
pipeline = IngestionPipeline(store=store)
|
||||
engine = SyncEngine(pipeline=pipeline)
|
||||
chunks = engine.sync(inst)
|
||||
def _run_sync() -> None:
|
||||
try:
|
||||
from openjarvis.connectors.pipeline import IngestionPipeline
|
||||
from openjarvis.connectors.store import KnowledgeStore
|
||||
from openjarvis.connectors.sync_engine import SyncEngine
|
||||
|
||||
store = KnowledgeStore()
|
||||
pipeline = IngestionPipeline(store=store)
|
||||
engine = SyncEngine(pipeline=pipeline)
|
||||
engine.sync(inst)
|
||||
logger.info("Sync completed for %s", connector_id)
|
||||
except Exception as exc:
|
||||
error_msg = str(exc)
|
||||
if "401" in error_msg or "Unauthorized" in error_msg:
|
||||
error_msg = "Authentication failed — credentials may have expired."
|
||||
elif "403" in error_msg or "Forbidden" in error_msg:
|
||||
error_msg = "Permission denied — check API scopes."
|
||||
elif "429" in error_msg or "Too Many Requests" in error_msg:
|
||||
error_msg = "Rate limited — wait a minute and try again."
|
||||
elif "timeout" in error_msg.lower():
|
||||
error_msg = "Connection timed out."
|
||||
logger.error("Sync failed for %s: %s", connector_id, error_msg)
|
||||
# Store error in sync_status if connector supports it
|
||||
try:
|
||||
inst._sync_error = error_msg
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
t = threading.Thread(target=_run_sync, daemon=True)
|
||||
t.start()
|
||||
_sync_threads[connector_id] = t
|
||||
|
||||
return {
|
||||
"connector_id": connector_id,
|
||||
"chunks_indexed": chunks,
|
||||
"status": "complete",
|
||||
"status": "started",
|
||||
}
|
||||
|
||||
@router.get("/{connector_id}/sync")
|
||||
|
||||
Reference in New Issue
Block a user