- Run an analysis to extract actionable items from your connected skills.
-
-
- >
- )}
+
Subconscious
+
+ OpenHuman will constantly have subconscious thoughts based on all the information
+ it has access to and the activity you have engaged with it in.
+
+
Coming soon
- ) : (
- /* Time Groups */
-
- {/* Inline analyzing indicator when refreshing with existing items */}
- {isRunning && (
-
-
- Analyzing your data…
-
- )}
- {timeGroups.map((group, groupIndex) => (
-
- {/* Group Header */}
-
-
{group.label}
-
- {group.count}
-
-
+ )}
- {/* Items */}
-
- {group.items.map((item, itemIndex) => (
-
-
-
- ))}
-
-
- ))}
+ {activeTab === 'dreams' && (
+
+
+
+
+
Dreams
+
+ Twice everyday, OpenHuman will generate a dream (or a summary) based on everything
+ that has happened in your life today. These dreams re then indexed and can be used
+ to influence OpenHuman's behavior.
+
+
Coming soon
)}
diff --git a/app/src/utils/desktopDeepLinkListener.ts b/app/src/utils/desktopDeepLinkListener.ts
index 368e24a21..89a6d4b2d 100644
--- a/app/src/utils/desktopDeepLinkListener.ts
+++ b/app/src/utils/desktopDeepLinkListener.ts
@@ -4,7 +4,7 @@ import { getCurrent, onOpenUrl } from '@tauri-apps/plugin-deep-link';
import { skillManager } from '../lib/skills/manager';
import { emitSkillStateChange } from '../lib/skills/skillEvents';
-import { setSetupComplete as rpcSetSetupComplete, startSkill } from '../lib/skills/skillsApi';
+import { startSkill } from '../lib/skills/skillsApi';
import { consumeLoginToken } from '../services/api/authApi';
import { store } from '../store';
import { setToken } from '../store/authSlice';
@@ -122,13 +122,8 @@ const handleOAuthDeepLink = async (parsed: URL) => {
console.log(`[DeepLink] OAuth success for skill=${skillId} integration=${integrationId}`);
- // 1. Persist setup completion
- await rpcSetSetupComplete(skillId, true).catch(err =>
- console.warn('[DeepLink] Failed to persist setup_complete via RPC:', err)
- );
- emitSkillStateChange(skillId);
-
- // 2. Start the skill in the core QuickJS runtime (if not already running)
+ // 1. Start the skill in the core QuickJS runtime (if not already running).
+ // This also sets enabled=true via the preferences store.
try {
await startSkill(skillId);
console.log(`[DeepLink] Skill '${skillId}' started in core runtime`);
@@ -136,7 +131,8 @@ const handleOAuthDeepLink = async (parsed: URL) => {
console.warn(`[DeepLink] Could not start skill '${skillId}' in runtime:`, startErr);
}
- // 3. Send oauth/complete to the running skill with the credential
+ // 2. Notify the running skill of the OAuth credential, mark setup_complete,
+ // and activate (list tools, sync to backend).
try {
await skillManager.notifyOAuthComplete(skillId, integrationId);
console.log(`[DeepLink] OAuth complete sent to skill '${skillId}'`);
@@ -144,7 +140,7 @@ const handleOAuthDeepLink = async (parsed: URL) => {
console.warn('[DeepLink] Runtime notify failed:', runtimeErr);
}
- // 4. Trigger initial data sync
+ // 3. Trigger initial data sync
try {
await skillManager.triggerSync(skillId);
} catch {
diff --git a/app/src/utils/tauriCommands.ts b/app/src/utils/tauriCommands.ts
index c8844c00d..9c37891ae 100644
--- a/app/src/utils/tauriCommands.ts
+++ b/app/src/utils/tauriCommands.ts
@@ -291,10 +291,15 @@ export async function memoryGraphQuery(
if (!isTauri()) {
throw new Error('Not running in Tauri');
}
- return await callCoreRpc({
+ const raw = await callCoreRpc({
method: 'openhuman.memory_graph_query',
params: { namespace, subject, predicate },
});
+ // RpcOutcome wraps with { result, logs } when logs are present — unwrap if needed.
+ if (Array.isArray(raw)) return raw;
+ if (raw && typeof raw === 'object' && 'result' in raw && Array.isArray(raw.result))
+ return raw.result;
+ return [];
}
export async function memoryDocIngest(params: {
diff --git a/src/openhuman/skills/preferences.rs b/src/openhuman/skills/preferences.rs
index 749194c8a..eaa55c918 100644
--- a/src/openhuman/skills/preferences.rs
+++ b/src/openhuman/skills/preferences.rs
@@ -100,8 +100,15 @@ impl PreferencesStore {
}
/// Set the setup completion flag for a skill. Persists immediately.
+ /// When marking setup as complete, also sets `enabled = true` so the skill
+ /// auto-starts on subsequent app launches.
pub fn set_setup_complete(&self, skill_id: &str, complete: bool) {
- self.update(skill_id, |p| p.setup_complete = complete);
+ self.update(skill_id, |p| {
+ p.setup_complete = complete;
+ if complete {
+ p.enabled = true;
+ }
+ });
log::info!(
"[preferences] setup_complete for '{}' set to {}",
skill_id,
@@ -114,10 +121,17 @@ impl PreferencesStore {
self.cache.read().clone()
}
- /// Resolve whether a skill should start, considering user preference and manifest default.
+ /// Resolve whether a skill should start, considering user preference,
+ /// setup completion, and manifest default.
+ ///
+ /// A skill with `setup_complete = true` always starts — the user explicitly
+ /// went through setup/OAuth, so the intent is to have it running.
+ /// Otherwise fall back to the explicit `enabled` preference, then the manifest default.
pub fn resolve_should_start(&self, skill_id: &str, manifest_auto_start: bool) -> bool {
- match self.is_enabled(skill_id) {
- Some(enabled) => enabled,
+ let pref = self.cache.read().get(skill_id).cloned();
+ match pref {
+ Some(p) if p.setup_complete => true,
+ Some(p) => p.enabled,
None => manifest_auto_start,
}
}
diff --git a/src/openhuman/skills/qjs_skill_instance/event_loop.rs b/src/openhuman/skills/qjs_skill_instance/event_loop.rs
index ad58e3018..37e1077fc 100644
--- a/src/openhuman/skills/qjs_skill_instance/event_loop.rs
+++ b/src/openhuman/skills/qjs_skill_instance/event_loop.rs
@@ -39,6 +39,7 @@ pub(crate) async fn run_event_loop(
timer_state: &Arc>,
ops_state: &Arc>,
memory_client: Option,
+ data_dir: &std::path::Path,
) {
// Maximum sleep duration when no timers are pending
const MAX_IDLE_SLEEP: Duration = Duration::from_millis(100);
@@ -76,6 +77,7 @@ pub(crate) async fn run_event_loop(
&mut pending_tool,
&memory_client,
ops_state,
+ data_dir,
)
.await;
if should_stop {
@@ -212,6 +214,7 @@ async fn handle_message(
pending_tool: &mut Option,
memory_client: &Option,
ops_state: &Arc>,
+ data_dir: &std::path::Path,
) -> bool {
match msg {
SkillMessage::CallTool {
@@ -226,7 +229,7 @@ async fn handle_message(
);
// Lazy-load persisted OAuth credential before calling the tool
- restore_oauth_credential(ctx, skill_id).await;
+ restore_oauth_credential(ctx, skill_id, data_dir).await;
log::debug!(
"[skill:{}] event_loop: OAuth credential restored for tool '{}'",
skill_id,
@@ -370,7 +373,7 @@ async fn handle_message(
let result = match method.as_str() {
"oauth/complete" => {
- // Set credential on the oauth bridge + persist to store
+ // Set credential on the oauth bridge + in-memory state
let cred_json =
serde_json::to_string(¶ms).unwrap_or_else(|_| "null".to_string());
let code = format!(
@@ -388,10 +391,21 @@ async fn handle_message(
let _ = js_ctx.eval::(code.as_bytes());
})
.await;
- log::info!(
- "[skill:{}] OAuth credential set and persisted to store",
- skill_id
- );
+
+ // Persist credential to disk so it survives restarts
+ let cred_path = data_dir.join("oauth_credential.json");
+ if let Err(e) = std::fs::write(&cred_path, &cred_json) {
+ log::error!(
+ "[skill:{}] Failed to persist OAuth credential: {e}",
+ skill_id
+ );
+ } else {
+ log::info!(
+ "[skill:{}] OAuth credential persisted to {}",
+ skill_id,
+ cred_path.display()
+ );
+ }
let params_str =
serde_json::to_string(¶ms).unwrap_or_else(|_| "{}".to_string());
handle_js_call(rt, ctx, "onOAuthComplete", ¶ms_str).await
@@ -445,7 +459,14 @@ async fn handle_message(
let _ = js_ctx.eval::(clear_code.as_bytes());
})
.await;
- log::info!("[skill:{}] OAuth credential cleared from store", skill_id);
+
+ // Remove persisted credential file
+ let cred_path = data_dir.join("oauth_credential.json");
+ let _ = std::fs::remove_file(&cred_path);
+ log::info!(
+ "[skill:{}] OAuth credential cleared from store and disk",
+ skill_id
+ );
// Fire-and-forget: delete memory for this integration
if let Some(client) = memory_client_opt {
diff --git a/src/openhuman/skills/qjs_skill_instance/instance.rs b/src/openhuman/skills/qjs_skill_instance/instance.rs
index 404baa8ff..066f7b09e 100644
--- a/src/openhuman/skills/qjs_skill_instance/instance.rs
+++ b/src/openhuman/skills/qjs_skill_instance/instance.rs
@@ -191,7 +191,7 @@ impl QjsSkillInstance {
return;
}
- restore_oauth_credential(&ctx, &config.skill_id).await;
+ restore_oauth_credential(&ctx, &config.skill_id, &data_dir).await;
// Call init() lifecycle
if let Err(e) = call_lifecycle(&rt, &ctx, "init").await {
@@ -242,6 +242,7 @@ impl QjsSkillInstance {
&timer_state,
&published_state,
_deps.memory_client.clone(),
+ &data_dir,
)
.await;
})
diff --git a/src/openhuman/skills/qjs_skill_instance/js_helpers.rs b/src/openhuman/skills/qjs_skill_instance/js_helpers.rs
index c821e084f..c46da97c9 100644
--- a/src/openhuman/skills/qjs_skill_instance/js_helpers.rs
+++ b/src/openhuman/skills/qjs_skill_instance/js_helpers.rs
@@ -77,25 +77,46 @@ pub(crate) fn extract_tools(js_ctx: &rquickjs::Ctx<'_>, state: &Arc s,
+ _ => return,
+ };
+
+ // Inject credential into both oauth bridge and in-memory state
+ let code = format!(
+ r#"(function() {{
+ var cred = {cred};
+ if (typeof globalThis.oauth !== 'undefined' && globalThis.oauth.__setCredential) {{
+ globalThis.oauth.__setCredential(cred);
+ }}
+ if (typeof globalThis.state !== 'undefined' && globalThis.state.set) {{
+ globalThis.state.set('__oauth_credential', cred);
+ }}
return true;
- }
- return false;
- })()"#;
+ }})()"#,
+ cred = cred_json
+ );
let restored = ctx
.with(|js_ctx| js_ctx.eval::(code.as_bytes()).unwrap_or(false))
.await;
if restored {
- log::info!("[skill:{}] Restored OAuth credential from store", skill_id);
+ log::info!(
+ "[skill:{}] Restored OAuth credential from {}",
+ skill_id,
+ cred_path.display()
+ );
}
}
diff --git a/src/openhuman/skills/quickjs_libs/bootstrap.js b/src/openhuman/skills/quickjs_libs/bootstrap.js
index 8413c1885..66c277c1a 100644
--- a/src/openhuman/skills/quickjs_libs/bootstrap.js
+++ b/src/openhuman/skills/quickjs_libs/bootstrap.js
@@ -867,6 +867,21 @@ globalThis.data = {
console.log('[oauth.fetch] ' + method + ' ' + proxyUrl + ' (credentialId=' + globalThis.__oauthCredential.credentialId + ')');
var result = await net.fetch(proxyUrl, fetchOpts);
console.log('[oauth.fetch] response status=' + result.status + ' body_len=' + (result.body ? result.body.length : 0));
+
+ // Auto-clear invalid/expired credentials so the user is prompted to re-auth
+ if (result.status === 401 || result.status === 403) {
+ console.warn('[oauth.fetch] Got ' + result.status + ' — clearing invalid credential for re-auth');
+ globalThis.__oauthCredential = null;
+ if (typeof globalThis.state !== 'undefined' && globalThis.state.set) {
+ globalThis.state.set('__oauth_credential', '');
+ globalThis.state.setPartial({
+ connection_status: 'error',
+ connection_error: 'Integration token expired or invalid. Please reconnect.',
+ auth_status: 'not_authenticated',
+ });
+ }
+ }
+
return result;
},
diff --git a/src/openhuman/skills/quickjs_libs/qjs_ops/ops_core.rs b/src/openhuman/skills/quickjs_libs/qjs_ops/ops_core.rs
index 0dd6b9044..9662584d2 100644
--- a/src/openhuman/skills/quickjs_libs/qjs_ops/ops_core.rs
+++ b/src/openhuman/skills/quickjs_libs/qjs_ops/ops_core.rs
@@ -7,6 +7,36 @@ use std::time::{Duration, Instant};
use super::types::{TimerEntry, TimerState, ALLOWED_ENV_VARS};
+/// Read the session JWT from the on-disk credentials store.
+///
+/// Returns `None` on any failure so the caller can fall back to env vars.
+fn token_from_credentials_store() -> Option {
+ use crate::openhuman::credentials::{AuthService, APP_SESSION_PROVIDER};
+
+ let home = directories::UserDirs::new()?.home_dir().to_path_buf();
+ let default_dir = home.join(".openhuman");
+
+ let state_dir = match std::env::var("OPENHUMAN_WORKSPACE") {
+ Ok(ws) if !ws.is_empty() => {
+ let ws_path = std::path::PathBuf::from(&ws);
+ if ws_path.join("config.toml").exists() {
+ ws_path
+ } else {
+ default_dir
+ }
+ }
+ _ => default_dir,
+ };
+
+ if !state_dir.exists() {
+ return None;
+ }
+
+ let auth = AuthService::new(&state_dir, true);
+ let profile = auth.get_profile(APP_SESSION_PROVIDER, None).ok()??;
+ profile.token.filter(|t| !t.trim().is_empty())
+}
+
pub fn register<'js>(
ctx: &Ctx<'js>,
ops: &Object<'js>,
@@ -115,6 +145,11 @@ pub fn register<'js>(
ops.set(
"get_session_token",
Function::new(ctx.clone(), || -> String {
+ // Try the on-disk credentials store first (where login actually persists
+ // the JWT), then fall back to the legacy JWT_TOKEN env var.
+ if let Some(token) = token_from_credentials_store() {
+ return token;
+ }
std::env::var("JWT_TOKEN").unwrap_or_default()
}),
)?;