@ateam-ai-mobile/core 0.1.0
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.
- package/dist/index.d.mts +389 -0
- package/dist/index.d.ts +389 -0
- package/dist/index.js +1081 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +1027 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +27 -0
- package/src/api/client.ts +105 -0
- package/src/api/mcp.ts +104 -0
- package/src/chat/store.ts +938 -0
- package/src/chat/types.ts +76 -0
- package/src/config.ts +118 -0
- package/src/index.ts +72 -0
- package/src/storage/types.ts +29 -0
- package/src/theme/tokens.ts +105 -0
- package/src/utils/emitter.ts +74 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/config.ts","../src/api/client.ts","../src/api/mcp.ts","../src/chat/store.ts","../src/utils/emitter.ts","../src/theme/tokens.ts"],"sourcesContent":["/**\n * SDK configuration — the single injection point between the host app and\n * the ADAS core. The host owns authentication, identity, and URL discovery;\n * the SDK reads everything through the config registered via initAdas().\n *\n * There is intentionally ONE active config per JS context (module singleton):\n * the chat store, WebSocket, and API client below it are app-wide singletons\n * too, and a second tenant/backend inside one app is not a supported v1 case.\n */\n\nimport type { AttachmentCache } from './storage/types';\n\n/** Result of a host-driven token refresh. Mirrors the semantics the API\n * client depends on: only 'auth_failed' logs the user out; 'transient'\n * (network blip, 5xx) keeps the session so the next call can retry. */\nexport interface RefreshOutcome {\n ok: 'ok' | 'auth_failed' | 'transient';\n status: number;\n reason?: string;\n}\n\n/**\n * Host-implemented auth bridge. The SDK never shows login UI and never\n * stores credentials — the host authenticates its own users and hands the\n * SDK a way to read/refresh the ADAS JWT it minted for them (via the\n * server-to-server token exchange).\n */\nexport interface TokenProvider {\n /** Current ADAS JWT, or null when signed out. Called on every request —\n * keep it synchronous and cheap (read from memory, not disk). */\n getToken(): string | null;\n /** Called on 401. Host should mint/refresh the ADAS token and return the\n * outcome; 'ok' makes the SDK retry the failed request once. */\n refresh(): Promise<RefreshOutcome>;\n /** Auth is genuinely dead (refresh rejected, or retry still 401). Host\n * should treat this as sign-out for the embedded experience. */\n onAuthFailure?: () => void;\n}\n\nexport interface AdasUser {\n actorId: string;\n name?: string;\n email?: string;\n}\n\nexport interface AdasConfig {\n /** Core API base URL, e.g. https://app.ateam-ai.com */\n coreUrl: string;\n /** Voice service base URL (optional — only voice features need it). */\n voiceUrl?: string;\n tokenProvider: TokenProvider;\n /** Tenant slug — informational/telemetry only. The backend derives the\n * tenant from the JWT; the SDK never sends a tenant header. */\n tenant?: string;\n /** Current end user as known to the host. actorId is used as the chat\n * reply context; without it, replies route to 'unknown'. */\n getUser?: () => AdasUser | null;\n /** Optional persistence for outgoing image/file attachments so previews\n * survive app restarts. No-op when omitted (previews live in memory for\n * the session only). @ateam-ai-mobile/ui ships an Expo/RN implementation. */\n attachmentCache?: AttachmentCache;\n}\n\nlet _config: AdasConfig | null = null;\n\n/** Register the SDK configuration. Call once at app start, before any other\n * SDK API. Calling again replaces the config (e.g. env switch) — combine\n * with chat.clearChat() and closeChatStream() for a clean swap. */\nexport function initAdas(config: AdasConfig): void {\n _config = config;\n}\n\nexport function isAdasInitialized(): boolean {\n return _config !== null;\n}\n\nexport function getConfig(): AdasConfig {\n if (!_config) {\n throw new Error('[adas] SDK not initialized — call initAdas(config) before using any ADAS API');\n }\n return _config;\n}\n\nexport function getCoreApiUrl(): string {\n return getConfig().coreUrl.replace(/\\/$/, '');\n}\n\nexport function getCoreWsUrl(): string {\n return getCoreApiUrl().replace(/^http/, 'ws');\n}\n\nexport function getVoiceApiUrl(): string {\n const url = getConfig().voiceUrl;\n if (!url) throw new Error('[adas] voiceUrl not configured');\n return url.replace(/\\/$/, '');\n}\n\nexport function getTenant(): string {\n return getConfig().tenant || '';\n}\n\nexport function getUser(): AdasUser | null {\n return getConfig().getUser?.() || null;\n}\n\n// ─── Internal token helpers (thin wrappers over the host's TokenProvider) ──\n\nexport function getToken(): string | null {\n return getConfig().tokenProvider.getToken();\n}\n\nexport async function refreshToken(): Promise<RefreshOutcome> {\n return getConfig().tokenProvider.refresh();\n}\n\nexport function notifyAuthFailure(): void {\n getConfig().tokenProvider.onAuthFailure?.();\n}\n","import { getCoreApiUrl, getVoiceApiUrl, getToken, refreshToken, notifyAuthFailure } from '../config';\n\ntype FetchOptions = RequestInit & {\n baseUrl?: 'core' | 'voice';\n};\n\n/**\n * Base API client with JWT injection and 401 refresh handling.\n *\n * Auth failure semantics (preserved from the reference app): a transient\n * 5xx or network blip on the RETRY after a successful refresh must NOT be\n * treated as sign-out. notifyAuthFailure() fires only when auth is genuinely\n * broken:\n * - Refresh itself was rejected with 401/403 → auth failure\n * - Retry with the fresh JWT still returns 401/403 → auth failure\n * Anything else throws but keeps the session intact — the caller may retry\n * on the next user action.\n */\nexport async function apiFetch<T = unknown>(\n path: string,\n options: FetchOptions = {},\n): Promise<T> {\n const { baseUrl = 'core', ...fetchOptions } = options;\n const base = baseUrl === 'voice' ? getVoiceApiUrl() : getCoreApiUrl();\n const url = `${base}${path}`;\n\n const token = getToken();\n\n const headers: Record<string, string> = {\n ...(fetchOptions.headers as Record<string, string>),\n };\n\n if (token && !headers['Authorization']) {\n headers['Authorization'] = `Bearer ${token}`;\n }\n\n // Note: we intentionally DO NOT send a tenant header. The backend derives\n // the tenant from the JWT — a client-supplied tenant paired with a leaked\n // JWT is a tenant-pivot footgun. Tenant comes from the token, not the client.\n\n if (!headers['Content-Type'] && fetchOptions.body && typeof fetchOptions.body === 'string') {\n headers['Content-Type'] = 'application/json';\n }\n\n const res = await fetch(url, { ...fetchOptions, headers });\n\n if (res.status === 401 && token) {\n console.warn(`[adas] 401 on ${path} — attempting refresh`);\n const outcome = await refreshToken();\n if (outcome.ok === 'ok') {\n const newToken = getToken();\n headers['Authorization'] = `Bearer ${newToken}`;\n const retryRes = await fetch(url, { ...fetchOptions, headers });\n if (retryRes.ok) {\n return retryRes.json() as Promise<T>;\n }\n if (retryRes.status === 401 || retryRes.status === 403) {\n // Still auth-broken with a fresh JWT — genuinely dead session.\n console.warn(`[adas] retry after refresh still ${retryRes.status} on ${path} — auth failure`);\n notifyAuthFailure();\n throw new Error('Session expired');\n }\n // Non-auth error on retry. Surface it, but the session is fine.\n const text = await retryRes.text().catch(() => '');\n throw new Error(`API ${retryRes.status}: ${text}`);\n }\n if (outcome.ok === 'auth_failed') {\n console.warn(`[adas] refresh auth_failed on ${path} (${outcome.reason}) — auth failure`);\n notifyAuthFailure();\n throw new Error('Session expired');\n }\n // 'transient' — network/5xx on refresh. Keep the session; surface the 401.\n throw new Error(`API 401: refresh transient (${outcome.reason})`);\n }\n\n if (!res.ok) {\n const text = await res.text();\n throw new Error(`API ${res.status}: ${text}`);\n }\n\n return res.json() as Promise<T>;\n}\n\n/**\n * Fetch with raw response (non-JSON responses, streaming bodies).\n */\nexport async function apiFetchRaw(\n path: string,\n options: FetchOptions = {},\n): Promise<Response> {\n const { baseUrl = 'core', ...fetchOptions } = options;\n const base = baseUrl === 'voice' ? getVoiceApiUrl() : getCoreApiUrl();\n const url = `${base}${path}`;\n\n const token = getToken();\n const headers: Record<string, string> = {\n ...(fetchOptions.headers as Record<string, string>),\n };\n\n if (token && !headers['Authorization']) {\n headers['Authorization'] = `Bearer ${token}`;\n }\n\n return fetch(url, { ...fetchOptions, headers });\n}\n","/**\n * Shared MCP client — single API layer for ALL plugin communication.\n *\n * Two call patterns:\n * 1. connectorCall(connectorId, tool, args) → POST /api/connectors/:id/call\n * 2. mcpFetch(body) → POST /mcp (raw JSON-RPC passthrough)\n */\nimport { apiFetchRaw } from './client';\n\n/**\n * Call a tool on a specific connector via POST /api/connectors/:id/call.\n *\n * NOTE: intentionally bypasses the /mcp proxy (which routes to adas-mcp).\n * adas-mcp only knows skill-generated tools, NOT raw connector tools like\n * triggers.list — the direct connector API is the reliable path.\n */\nexport async function connectorCall(\n connectorId: string,\n tool: string,\n args: Record<string, any> = {},\n): Promise<{ ok: boolean; result: any; error?: string }> {\n try {\n const res = await apiFetchRaw(\n `/api/connectors/${encodeURIComponent(connectorId)}/call`,\n {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ tool, args }),\n },\n );\n\n if (!res.ok) {\n const text = await res.text();\n return { ok: false, result: null, error: `HTTP ${res.status}: ${text}` };\n }\n\n const data = await res.json();\n // Normalize: Core may return { ok, result } or raw MCP { content: [...] }\n if (data.ok !== undefined) return data;\n return { ok: true, result: data };\n } catch (err) {\n return { ok: false, result: null, error: err instanceof Error ? err.message : String(err) };\n }\n}\n\n/** Raw JSON-RPC passthrough to /mcp. Returns the full JSON-RPC response. */\nexport async function mcpFetch(body: any): Promise<any> {\n const res = await apiFetchRaw('/mcp', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: typeof body === 'string' ? body : JSON.stringify(body),\n });\n\n if (!res.ok) {\n const text = await res.text();\n throw new Error(`MCP fetch failed: HTTP ${res.status}: ${text}`);\n }\n\n return res.json();\n}\n\n/** JSON-RPC tools/call to /mcp (for core tools like cp.fe_api). */\nexport async function mcpToolCall(\n toolName: string,\n args: Record<string, any> = {},\n): Promise<any> {\n return mcpFetch({\n jsonrpc: '2.0',\n id: Date.now(),\n method: 'tools/call',\n params: { name: toolName, arguments: args },\n });\n}\n\n/** Post a plugin command result back to Core. */\nexport async function postCommandResult(\n correlationId: string,\n result: any,\n error?: string,\n): Promise<void> {\n await apiFetchRaw('/api/plugin-command-result', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ correlationId, result, error }),\n });\n}\n\n/**\n * Unwrap MCP JSON-RPC text content → parsed business data.\n * Server returns: { content: [{ type: \"text\", text: \"{...}\" }] }\n */\nexport function unwrapMcpResult(result: any): any {\n if (!result) return result;\n if (!result.content) return result;\n const textItem = Array.isArray(result.content)\n ? result.content.find((c: any) => c.type === 'text')\n : null;\n if (!textItem?.text) return result;\n try {\n return JSON.parse(textItem.text);\n } catch {\n return textItem.text;\n }\n}\n","/**\n * Chat store — talks to the ADAS Core API.\n *\n * Flow:\n * 1. loadConversation() → GET /api/conversation → display history\n * 2. sendMessage(text) → POST /api/chat { goal } → returns jobId\n * 3. WebSocket (poll fallback) → real-time progress\n * 4. When done → GET /api/conversation → formatted response\n *\n * Platform notes: no Expo/AsyncStorage imports. Attachment persistence is\n * delegated to the host via config.attachmentCache; identity and tokens come\n * from the host via initAdas().\n */\nimport { create } from 'zustand';\nimport { getCoreApiUrl, getCoreWsUrl, getToken, getTenant, getUser, getConfig } from '../config';\nimport { pluginCommandEmitter } from '../utils/emitter';\nimport type { OutgoingAttachment } from '../storage/types';\nimport type { Message, MessageAttachment, JobProgress, UIAction } from './types';\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\ninterface ChatState {\n messages: Message[];\n loading: boolean;\n error: string | null;\n activeJobId: string | null;\n conversationLoaded: boolean;\n /** Load conversation history from Core */\n loadConversation: () => Promise<void>;\n /** Send a message via POST /api/chat — creates a job, streams progress */\n sendMessage: (text: string, skillSlug?: string, attachments?: OutgoingAttachment[]) => Promise<void>;\n /** Respond to a paused sys.askUser question */\n respondToQuestion: (jobId: string, inputId: string, answer: string) => Promise<void>;\n /** Update in-flight job progress (called by the stream handler) */\n updateJobProgress: (jobId: string, progress: Partial<JobProgress>) => void;\n /** Reset chat (also re-fetches history on next mount — used on auth switch) */\n clearChat: () => void;\n /** Start a fresh empty chat without re-fetching history */\n newChat: () => void;\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\nfunction coreHeaders(): Record<string, string> {\n const token = getToken();\n const headers: Record<string, string> = {\n 'Content-Type': 'application/json',\n };\n if (token) headers['Authorization'] = `Bearer ${token}`;\n return headers;\n}\n\nfunction isTerminalStatus(status?: unknown): boolean {\n const s = typeof status === 'string' ? status.toLowerCase() : '';\n return s === 'done' || s === 'error' || s === 'failed' || s === 'completed';\n}\n\nfunction normalizeJobStatus(snap: any): JobProgress['status'] {\n const status = typeof snap?.status === 'string' ? snap.status.toLowerCase() : '';\n if (status === 'done' || status === 'completed') return 'done';\n if (status === 'error' || status === 'failed') return 'error';\n if (status === 'paused' || status === 'waiting') return 'paused';\n if (status === 'unknown') return 'unknown';\n return 'running';\n}\n\nfunction extractPendingQuestion(snap: any): any {\n return snap?.pendingQuestion || snap?.state?.pendingQuestion || snap?.state?.waiting || null;\n}\n\nfunction extractActorId(snap: any): string | undefined {\n const actorId =\n snap?.actorId ||\n snap?.actor?.actorId ||\n snap?.actor?.id ||\n snap?.state?.actorId ||\n snap?.state?.actor?.actorId ||\n snap?.state?.actor?.id;\n return typeof actorId === 'string' && actorId.length > 0 ? actorId : undefined;\n}\n\nfunction extractLiveProgress(snap: any): JobProgress['liveProgress'] | undefined {\n const lp = snap?.liveProgress || snap?.state?.__live_progress;\n const latest = Array.isArray(lp) ? lp[lp.length - 1] : lp;\n if (!latest?.message) return undefined;\n return {\n message: String(latest.message || ''),\n ...(latest.step != null ? { step: latest.step } : {}),\n ...(latest.total != null ? { total: latest.total } : {}),\n };\n}\n\n// ---------------------------------------------------------------------------\n// Extract the most-recently-called tool from job exec steps.\n// snap.state.steps.__exec = { iter_0: [{tool, ok, reason}], iter_1: [...] }\n// ---------------------------------------------------------------------------\nfunction extractCurrentTool(state: any): string | undefined {\n const exec = state?.steps?.__exec;\n if (!exec || typeof exec !== 'object') return undefined;\n const latest = Object.keys(exec)\n .map(k => ({ k, n: parseInt(k.replace('iter_', ''), 10) }))\n .filter(({ n }) => !isNaN(n))\n .sort((a, b) => b.n - a.n)[0];\n if (!latest) return undefined;\n const steps = exec[latest.k];\n if (!Array.isArray(steps) || !steps.length) return undefined;\n const last = steps[steps.length - 1];\n return (last?.tool || last?.phase) as string | undefined;\n}\n\nfunction snapshotToProgress(snap: any, rootJobId: string): Partial<JobProgress> {\n const pq = extractPendingQuestion(snap);\n const progress: Partial<JobProgress> = {\n jobId: rootJobId,\n actorId: extractActorId(snap),\n status: pq ? 'paused' : normalizeJobStatus(snap),\n subStatus: snap?.subStatus || snap?.phase || snap?.state?.visual?.phase,\n phase: snap?.phase || snap?.state?.visual?.phase,\n iteration: snap?.iteration || snap?.state?.plannerIter,\n skillSlug: snap?.skillSlug || snap?.activeSkill,\n currentTool: snap?.currentTool || snap?.lastTool || extractCurrentTool(snap?.state),\n };\n\n const liveProgress = extractLiveProgress(snap);\n if (liveProgress) progress.liveProgress = liveProgress;\n\n if (pq) {\n progress.pendingQuestion = {\n inputId: pq.correlation_id || pq.correlationId || pq.inputId || '',\n question: pq.prompt_message || pq.question || 'The assistant needs your input',\n inputType: pq.inputType || 'text',\n options: pq.options || pq.requested_fields,\n placeholder: pq.placeholder,\n };\n }\n\n return progress;\n}\n\nfunction isTerminalSnapshot(snap: any): boolean {\n if (extractPendingQuestion(snap)) return false;\n return snap?.done === true || isTerminalStatus(snap?.status);\n}\n\n/**\n * Sentinel delimiters used to embed structured table data inside a flat\n * message string. UI layers split on these and render the contents with a\n * native table component instead of markdown tables — markdown tables look\n * terrible on narrow phone screens.\n *\n * Unicode Private Use Area chars (U+E100/U+E101) so they never collide with\n * anything the model could legitimately emit.\n */\nexport const TABLE_BLOCK_OPEN = '\\uE100';\nexport const TABLE_BLOCK_CLOSE = '\\uE101';\n\n/**\n * Render a `{ columns: [...], rows: [...] }` payload as a sentinel-wrapped\n * JSON marker. Returns null if the shape doesn't match.\n */\nfunction renderTableShape(content: any): string | null {\n if (!content || typeof content !== 'object') return null;\n const cols = Array.isArray(content.columns) ? content.columns : null;\n const rows = Array.isArray(content.rows) ? content.rows : null;\n if (!cols || !rows) return null;\n // encodeURIComponent so the JSON can never contain newlines or sentinel chars.\n const payload = encodeURIComponent(JSON.stringify({ columns: cols, rows }));\n return `${TABLE_BLOCK_OPEN}${payload}${TABLE_BLOCK_CLOSE}`;\n}\n\n/**\n * Extract display text from a conversation turn's assistant blocks.\n * Handles multiple block kinds (text, code, json, markdown, table, html).\n */\nexport function blocksToText(blocks?: Array<{ kind: string; content: any }>): string {\n if (!blocks || blocks.length === 0) return '';\n return blocks.map(b => {\n if (b.kind === 'text' || b.kind === 'markdown' || b.kind === 'description') {\n return typeof b.content === 'string' ? b.content : '';\n }\n if (b.kind === 'code') {\n const code = typeof b.content === 'object' ? b.content.content : b.content;\n return `\\`\\`\\`\\n${code}\\n\\`\\`\\``;\n }\n if (b.kind === 'table' && b.content && typeof b.content === 'object') {\n const md = renderTableShape(b.content);\n if (md) return md;\n // Fall through to generic JSON rendering if shape is unexpected.\n }\n if (b.kind === 'json') {\n // Skip pendingQuestion state objects — internal sys.askUser data\n if (b.content && typeof b.content === 'object' && 'pendingQuestion' in b.content) return '';\n // Polisher sometimes wraps tables as kind:'json' when the content has\n // columns/rows — render those as table blocks too.\n const md = renderTableShape(b.content);\n if (md) return md;\n return '```json\\n' + JSON.stringify(b.content, null, 2) + '\\n```';\n }\n if (b.kind === 'html') {\n return typeof b.content === 'string' ? b.content : '';\n }\n return typeof b.content === 'string' ? b.content : JSON.stringify(b.content);\n }).filter(Boolean).join('\\n\\n');\n}\n\n/** Extract image/file attachments from conversation blocks */\nfunction blocksToAttachments(blocks?: Array<{ kind: string; hash?: string; url?: string; mimeType?: string; name?: string }>): MessageAttachment[] | undefined {\n if (!blocks || blocks.length === 0) return undefined;\n const atts: MessageAttachment[] = [];\n for (const b of blocks) {\n if ((b.kind === 'image' || b.kind === 'file') && (b.url || b.hash)) {\n atts.push({\n mimeType: b.mimeType || (b.kind === 'image' ? 'image/jpeg' : 'application/octet-stream'),\n name: b.name || 'attachment',\n previewUri: b.url ? `${getCoreApiUrl()}${b.url}` : undefined,\n });\n }\n }\n return atts.length > 0 ? atts : undefined;\n}\n\n// ---------------------------------------------------------------------------\n// Store\n// ---------------------------------------------------------------------------\n\nexport const useChatStore = create<ChatState>((set, get) => ({\n messages: [],\n loading: false,\n error: null,\n activeJobId: null,\n conversationLoaded: false,\n\n // -----------------------------------------------------------------------\n // Load conversation history from Core\n // GET /api/conversation → array of turns [{user, assistant, jobId, ...}]\n // -----------------------------------------------------------------------\n loadConversation: async () => {\n try {\n const attachmentCache = getConfig().attachmentCache;\n await attachmentCache?.ready();\n\n const res = await fetch(`${getCoreApiUrl()}/api/conversation?limit=20`, {\n headers: coreHeaders(),\n });\n if (!res.ok) {\n console.warn('[adas-chat] Failed to load conversation:', res.status);\n set({ conversationLoaded: true });\n return;\n }\n\n const data = await res.json();\n if (!data.ok || !data.items) {\n set({ conversationLoaded: true });\n return;\n }\n\n // Persisted turns from /api/conversation are the SOLE source of history.\n // Live job state is mirrored only via WS into the placeholder bubble\n // created by sendMessage; in-flight local state is preserved below.\n const items: any[] = [...data.items].reverse();\n const persistedMessages: Message[] = [];\n const seenJobIds = new Set<string>();\n for (const turn of items) {\n // Deduplicate by jobId (chain jobs can produce duplicate turns)\n if (turn.jobId && seenJobIds.has(turn.jobId)) continue;\n if (turn.jobId) seenJobIds.add(turn.jobId);\n\n // User message\n const userText = blocksToText(turn.user?.blocks);\n let userAtts = blocksToAttachments(turn.user?.blocks);\n // Merge locally cached attachments if API doesn't have them\n if (!userAtts && turn.jobId) {\n userAtts = attachmentCache?.get(turn.jobId);\n }\n const turnChainId = typeof turn.chainId === 'string' ? turn.chainId : undefined;\n if (userText || userAtts) {\n persistedMessages.push({\n id: `user-${turn.jobId}`,\n role: 'user',\n content: userText || '',\n timestamp: new Date(turn.startedAt || Date.now()),\n skillSlug: turn.skillSlug,\n ...(userAtts ? { attachments: userAtts } : {}),\n ...(turnChainId ? { chainId: turnChainId } : {}),\n });\n }\n // Assistant response\n const assistantText = blocksToText(turn.assistant?.blocks);\n const assistantAtts = blocksToAttachments(turn.assistant?.blocks);\n const assistantActions = Array.isArray(turn.assistant?.actions) ? turn.assistant.actions : undefined;\n const turnKind = typeof turn.kind === 'string' ? turn.kind : undefined;\n if (assistantText || assistantAtts || assistantActions?.length) {\n persistedMessages.push({\n id: `bot-${turn.jobId}`,\n role: 'assistant',\n content: assistantText || '',\n timestamp: new Date(turn.finishedAt || turn.startedAt || Date.now()),\n skillSlug: turn.skillSlug,\n ...(assistantAtts ? { attachments: assistantAtts } : {}),\n ...(assistantActions?.length ? { actions: assistantActions } : {}),\n ...(turnChainId ? { chainId: turnChainId } : {}),\n ...(turnKind ? { kind: turnKind } : {}),\n });\n }\n }\n\n // Preserve in-flight LOCAL state across pull-to-refresh. /api/conversation\n // is the truth for HISTORY; anything mid-flight lives only as a local\n // WS-driven placeholder created by sendMessage.\n const current = get().messages;\n const keptPlaceholders: Message[] = [];\n for (const m of current) {\n if (!m.id.startsWith('job-') || !m.jobProgress || !m.chainId) continue;\n const matchIdx = persistedMessages.findIndex(pm =>\n pm.role === 'assistant' && pm.chainId === m.chainId\n );\n if (matchIdx >= 0) {\n // The persisted turn for this chain has arrived. Move the live\n // progress overlay (pill) onto it — but only while still in flight.\n const st = m.jobProgress.status;\n if (st === 'running' || st === 'paused') {\n persistedMessages[matchIdx] = { ...persistedMessages[matchIdx], jobProgress: m.jobProgress };\n }\n // Drop the placeholder either way.\n } else {\n // No persisted turn yet — chain still in flight. Keep it.\n keptPlaceholders.push(m);\n }\n }\n\n // Optimistic user bubbles (id=`user-<ms>` from sendMessage). Keep until\n // a persisted user turn arrives. Dedup by chainId — content match has\n // false positives (\"Yes\", \"Continue\", etc.).\n const isLocalUserId = (id: string) => /^user-\\d+$/.test(id);\n const keptUserBubbles = current.filter(m => {\n if (m.role !== 'user' || !isLocalUserId(m.id)) return false;\n if (!m.chainId) return true; // POST in flight — can't dedup yet\n return !persistedMessages.some(pm =>\n pm.role === 'user' && pm.chainId === m.chainId\n );\n });\n\n const finalMessages = [...persistedMessages, ...keptUserBubbles, ...keptPlaceholders];\n finalMessages.sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());\n\n let firstRunning: Message | undefined = keptPlaceholders.find(m => m.jobProgress?.status === 'running');\n\n // Cold-start / post-refresh reattach: the WS subscription dies on app\n // restart. /api/chain/current is the single hydration endpoint that\n // says \"is a chain still in flight for this actor?\". If yes, re-open\n // the live subscription on the same chainId so the pill comes back.\n try {\n const chainRes = await fetch(`${getCoreApiUrl()}/api/chain/current`, {\n headers: coreHeaders(),\n });\n if (chainRes.ok) {\n const chainBody = await chainRes.json();\n const slim = chainBody?.data;\n if (slim?.id) {\n const chainId = String(slim.id);\n const alreadyTracked = finalMessages.some(m =>\n m.role === 'assistant' && m.jobProgress?.jobId === chainId\n );\n if (!alreadyTracked) {\n const progress = snapshotToProgress(slim, chainId);\n const chainCreatedAt = typeof slim.createdAt === 'number'\n ? slim.createdAt\n : (typeof slim.lastUpdate === 'number' ? slim.lastUpdate : Date.now());\n const chainLastUpdate = typeof slim.lastUpdate === 'number' ? slim.lastUpdate : chainCreatedAt;\n const jobProgress = {\n jobId: chainId,\n actorId: extractActorId(slim),\n status: 'running',\n chainStartedAt: chainCreatedAt,\n ...progress,\n } as JobProgress;\n\n // Synthesize the user bubble if a hard refresh wiped the\n // optimistic one and the chain hasn't persisted its turn yet.\n const goal = typeof slim.goal === 'string' ? slim.goal : '';\n const userExists = finalMessages.some(m =>\n m.role === 'user' && (m.chainId === chainId || (goal && m.content === goal))\n );\n if (goal && !userExists) {\n finalMessages.push({\n id: `user-${chainId}`,\n role: 'user',\n content: goal,\n timestamp: new Date(chainCreatedAt),\n chainId,\n });\n }\n\n // Assistant side: attach to matching persisted turn if one\n // exists; otherwise push a placeholder bubble.\n let matchIdx = -1;\n for (let i = finalMessages.length - 1; i >= 0; i--) {\n const m = finalMessages[i];\n if (m.role === 'assistant' && m.chainId === chainId) {\n matchIdx = i;\n break;\n }\n }\n if (matchIdx >= 0) {\n finalMessages[matchIdx] = { ...finalMessages[matchIdx], jobProgress };\n if (jobProgress.status === 'running') firstRunning = finalMessages[matchIdx];\n } else {\n const reattachBubble: Message = {\n id: `job-${chainId}`,\n role: 'assistant',\n content: 'Working on it...',\n timestamp: new Date(chainLastUpdate),\n jobProgress,\n chainId,\n };\n finalMessages.push(reattachBubble);\n if (jobProgress.status === 'running') firstRunning = reattachBubble;\n }\n\n finalMessages.sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());\n\n // Re-open the live subscription.\n streamJob(chainId, getTenant(), getToken());\n }\n }\n }\n } catch {}\n\n set({\n messages: finalMessages,\n conversationLoaded: true,\n error: null,\n activeJobId: firstRunning?.jobProgress?.jobId || null,\n loading: !!firstRunning,\n });\n } catch (err: any) {\n console.warn('[adas-chat] Error loading conversation:', err.message);\n set({ conversationLoaded: true });\n }\n },\n\n // -----------------------------------------------------------------------\n // Send message via Core API\n // POST /api/chat { goal } → { jobId } → stream → fetch turn result\n // -----------------------------------------------------------------------\n sendMessage: async (text: string, skillSlug?: string, attachments?: OutgoingAttachment[]) => {\n // No `if (loading) return` guard: Chain Additions needs the user to send\n // WHILE a chain is running. The backend router decides attach/cancel/new.\n\n const userMsg: Message = {\n id: `user-${Date.now()}`,\n role: 'user',\n content: text,\n timestamp: new Date(),\n ...(attachments?.length ? {\n attachments: attachments.map(a => ({\n mimeType: a.mimeType,\n name: a.name,\n previewUri: a.mimeType.startsWith('image/') ? `data:${a.mimeType};base64,${a.data}` : undefined,\n }))\n } : {}),\n };\n\n set(s => ({ messages: [...s.messages, userMsg], loading: true, error: null }));\n\n try {\n // Backend auto-resolves the skill via conversation continuity → channel\n // routing → default. triggerContext.channel + replyContext.chatId are\n // required by handoff-controller-mcp's schema — without them handoffs\n // fail silently.\n const actor = getUser();\n const body: Record<string, unknown> = {\n goal: text,\n // Opt in to the Chain Additions router (attach/cancel/new while a\n // chain runs). Backend ignores it where not applicable — safe to send.\n chainRouting: true,\n triggerContext: {\n channel: 'mobile',\n replyContext: { chatId: actor?.actorId || 'unknown' },\n },\n };\n if (skillSlug) body.skillSlug = skillSlug;\n if (attachments?.length) body.attachments = attachments;\n\n const res = await fetch(`${getCoreApiUrl()}/api/chat`, {\n method: 'POST',\n headers: coreHeaders(),\n body: JSON.stringify(body),\n });\n\n if (!res.ok) {\n let errMsg = `Server error (${res.status})`;\n try {\n const text = await res.text();\n // Don't show raw HTML — extract a short message\n if (text.startsWith('<') || text.startsWith('<!')) {\n errMsg = res.status === 502 ? 'Server is temporarily unavailable. Please try again.' : `Server error (${res.status})`;\n } else {\n const parsed = JSON.parse(text);\n errMsg = parsed.error || errMsg;\n }\n } catch {}\n throw new Error(errMsg);\n }\n\n const data = await res.json();\n\n if (!data.ok) {\n throw new Error(data.error || 'Failed to send message');\n }\n\n // Chain Additions: the router may ATTACH this message to the running\n // chain or CANCEL it. These replies carry NO jobId — handle them and\n // don't start a second stream.\n if (data.mode === 'chain_addition' || data.mode === 'chain_cancel') {\n const ack = data.mode === 'chain_addition'\n ? (data.ack || 'Got it — added to the running task.')\n : 'Stopping the task…';\n set(s => ({\n messages: s.messages\n .map(m => (m.id === userMsg.id && data.chainId) ? { ...m, chainId: data.chainId } : m)\n .concat({\n id: `ack-${Date.now()}`,\n role: 'assistant',\n content: ack,\n timestamp: new Date(),\n ...(data.chainId ? { chainId: data.chainId } : {}),\n }),\n }));\n return;\n }\n\n const jobId = data.id || data.jobId;\n if (!jobId) {\n throw new Error('No job ID returned');\n }\n\n // Cache attachments so they survive loadConversation reload. Awaited\n // intentionally — fire-and-forget can lose the write on quick reload.\n if (attachments?.length) {\n await getConfig().attachmentCache?.save(jobId, attachments);\n }\n\n // Placeholder assistant bubble = live mirror of the active chain.\n // Carries chainId so a subsequent loadConversation refresh can adopt\n // this placeholder's progress. For the root job, chainId == jobId.\n const progressMsg: Message = {\n id: `job-${jobId}`,\n role: 'assistant',\n content: 'Working on it...',\n timestamp: new Date(),\n jobProgress: {\n jobId,\n actorId: actor?.actorId,\n status: 'running',\n subStatus: 'Starting...',\n chainStartedAt: Date.now(),\n },\n chainId: jobId,\n };\n\n // Attach chainId to the optimistic user bubble so loadConversation can\n // dedup against persisted user turns by chainId.\n set(s => ({\n messages: s.messages.map(m =>\n m.id === userMsg.id ? { ...m, chainId: jobId } : m\n ).concat(progressMsg),\n activeJobId: jobId,\n }));\n\n // Start streaming for real-time progress\n streamJob(jobId, getTenant(), getToken());\n } catch (err: any) {\n set(s => ({\n messages: [...s.messages, {\n id: `error-${Date.now()}`,\n role: 'system' as const,\n content: `Error: ${err.message}`,\n timestamp: new Date(),\n }],\n loading: false,\n }));\n }\n },\n\n // -----------------------------------------------------------------------\n // Respond to sys.askUser paused question\n // -----------------------------------------------------------------------\n respondToQuestion: async (jobId: string, inputId: string, answer: string) => {\n try {\n const res = await fetch(`${getCoreApiUrl()}/api/job/${jobId}/respond`, {\n method: 'POST',\n headers: coreHeaders(),\n body: JSON.stringify({ inputId, answer }),\n });\n const data = await res.json();\n if (!data.ok) {\n console.warn('[adas-chat] Failed to respond to question:', data.error);\n }\n } catch (err: any) {\n console.warn('[adas-chat] Error responding to question:', err.message);\n }\n },\n\n // -----------------------------------------------------------------------\n // Update job progress (called by the stream handler)\n // -----------------------------------------------------------------------\n updateJobProgress: (jobId: string, progress: Partial<JobProgress>) => {\n set(s => ({\n messages: s.messages.map(m => {\n if (m.jobProgress?.jobId === jobId) {\n const updated = { ...m.jobProgress, ...progress };\n // Clear pendingQuestion when status is no longer paused\n if (progress.status && progress.status !== 'paused') {\n updated.pendingQuestion = undefined;\n }\n // Persisted turns (id `bot-…`) own their content/actions from\n // /api/conversation — the live mirror must never overwrite them.\n const isPersisted = m.id.startsWith('bot-');\n const content = isPersisted\n ? m.content\n : (progress.resultMessage\n || progress.pendingQuestion?.question\n || m.content);\n const actions = isPersisted\n ? m.actions\n : (progress.resultActions?.length ? progress.resultActions : m.actions);\n return {\n ...m,\n jobProgress: updated as JobProgress,\n content,\n ...(actions ? { actions } : {}),\n };\n }\n return m;\n }),\n loading: (progress.status === 'done' || progress.status === 'error' || progress.status === 'paused')\n ? false\n : s.loading,\n activeJobId: (progress.status === 'done' || progress.status === 'error' || progress.status === 'paused')\n ? null\n : s.activeJobId,\n }));\n },\n\n clearChat: () => {\n set({\n messages: [],\n loading: false,\n error: null,\n activeJobId: null,\n conversationLoaded: false,\n });\n },\n newChat: () => {\n set({\n messages: [],\n loading: false,\n error: null,\n activeJobId: null,\n conversationLoaded: true,\n });\n },\n}));\n\n// ---------------------------------------------------------------------------\n// WebSocket Streaming — persistent connection, reused across jobs\n// ---------------------------------------------------------------------------\n\n// Track processed plugin command correlationIds to avoid duplicates\nconst _processedPluginCmds = new Set<string>();\n\nfunction dispatchPluginCommands(cmds: any[]) {\n for (const cmd of cmds) {\n const corrId = cmd?.correlationId || cmd?.correlation_id;\n if (corrId && _processedPluginCmds.has(corrId)) continue;\n if (corrId) _processedPluginCmds.add(corrId);\n\n pluginCommandEmitter.emit('plugin-command', cmd);\n const pluginId = cmd?.pluginId || cmd?.plugin_id || cmd?.args?.plugin_id;\n if (pluginId) {\n const props = cmd?.props ?? cmd?.args?.props ?? undefined;\n pluginCommandEmitter.emit('focus-plugin', { pluginId, props });\n }\n }\n}\n\n// Persistent WS connection — stays open across jobs\nlet _ws: WebSocket | null = null;\nlet _wsReady = false;\nlet _currentJobId: string | null = null;\n\nfunction ensureWs(tenant: string, token: string | null): WebSocket {\n if (_ws && _wsReady) return _ws;\n\n const wsUrl = `${getCoreWsUrl()}/ws?token=${encodeURIComponent(token || '')}&tenant=${encodeURIComponent(tenant)}`;\n const ws = new WebSocket(wsUrl);\n _ws = ws;\n _wsReady = false;\n\n ws.onopen = () => {\n _wsReady = true;\n // If a job is waiting for the connection, subscribe now\n if (_currentJobId) {\n ws.send(JSON.stringify({ type: 'subscribe', jobId: _currentJobId }));\n }\n };\n\n ws.onmessage = (ev) => {\n try {\n const msg = JSON.parse(typeof ev.data === 'string' ? ev.data : '');\n const { event, data } = msg;\n const { updateJobProgress } = useChatStore.getState();\n // Route by the event's OWN chain id — the server streams ALL of the\n // user's active chains down one connection, so each event updates its\n // own bubble. Fall back to the last-subscribed job for older backends.\n const jobId = (data && (data.id || data.chainId)) || _currentJobId;\n\n // Always process plugin_command events even when _currentJobId is null\n // (e.g., during handoff relay)\n if (event === 'plugin_command') {\n dispatchPluginCommands([data]);\n return;\n }\n if (event === 'plugin-command-ack' || event === 'pong') return;\n\n if (!jobId) return;\n\n switch (event) {\n case 'init':\n case 'job':\n case 'update': {\n const snap = data;\n const progress = snapshotToProgress(snap, jobId);\n\n if (isTerminalSnapshot(snap)) {\n // Handoff chain: root job finished but relay job is still running.\n // The backend broadcasts relay progress as synthetic updates on\n // this same subscription — stay subscribed and show running.\n if (snap.status === 'done' && (snap.isHandoff || snap.state?.handoffTransfer)) {\n updateJobProgress(jobId, { status: 'running' });\n return;\n }\n _currentJobId = null;\n if (progress.status === 'paused' && progress.pendingQuestion) {\n updateJobProgress(jobId, progress);\n } else {\n fetchTurnResult(jobId, snap);\n }\n return;\n }\n\n updateJobProgress(jobId, progress);\n break;\n }\n case 'chain_final':\n // Authoritative chain-termination event (fires once when the WHOLE\n // chain is done — handoff-forgiven). Preferred over per-job `final`.\n _currentJobId = null;\n fetchTurnResult(jobId, data);\n break;\n case 'final':\n _currentJobId = null;\n fetchTurnResult(jobId, data);\n break;\n }\n } catch (err) {\n console.warn('[adas-chat] WS parse error:', err);\n }\n };\n\n ws.onerror = () => {\n _ws = null;\n _wsReady = false;\n // Fall back to polling for current job\n if (_currentJobId) {\n const jid = _currentJobId;\n _currentJobId = null;\n pollJob(jid, token);\n }\n };\n\n ws.onclose = () => {\n _ws = null;\n _wsReady = false;\n if (_currentJobId) {\n const jid = _currentJobId;\n _currentJobId = null;\n pollJob(jid, token);\n }\n };\n\n return ws;\n}\n\nfunction streamJob(jobId: string, tenant: string, token: string | null) {\n _currentJobId = jobId;\n const ws = ensureWs(tenant, token);\n\n // If WS is already open, subscribe immediately; otherwise onopen handles it\n if (_wsReady) {\n ws.send(JSON.stringify({ type: 'subscribe', jobId }));\n }\n}\n\n/** Tear down the live connection (env/auth switch, SDK unmount). */\nexport function closeChatStream(): void {\n _currentJobId = null;\n if (_ws) {\n try { _ws.close(); } catch {}\n }\n _ws = null;\n _wsReady = false;\n}\n\n/**\n * When a job completes, fetch the conversation turn to get the formatted\n * response. Falls back to extracting from the job snapshot.\n */\nasync function fetchTurnResult(jobId: string, jobSnap: any) {\n // Clean up dedup set (cap to prevent unbounded growth)\n if (_processedPluginCmds.size > 200) _processedPluginCmds.clear();\n const { updateJobProgress } = useChatStore.getState();\n const status = normalizeJobStatus(jobSnap) === 'error' ? 'error' : 'done';\n\n // Handoff chain: if this job handed off, wait for the chain to settle by\n // polling job status — done:true only when the relay job completes.\n if (jobSnap.isHandoff || jobSnap.state?.handoffTransfer || jobSnap.result?.contentType === 'handoff') {\n try {\n for (let i = 0; i < 40; i++) { // up to ~60s\n const statusRes = await fetch(`${getCoreApiUrl()}/api/job/${jobId}/status`, {\n headers: coreHeaders(),\n });\n if (statusRes.ok) {\n const statusData = await statusRes.json();\n if (statusData.done && !statusData.isHandoff) {\n break;\n }\n updateJobProgress(jobId, { status: 'running' });\n }\n await new Promise(r => setTimeout(r, 1500));\n }\n } catch {}\n }\n\n // Fetch the most recent conversation turn. By actor rather than by jobId,\n // because handoff chains create child jobs — the real reply lives on the\n // LAST child, not on the root job we submitted.\n for (let attempt = 0; attempt < 5; attempt++) {\n try {\n const res = await fetch(`${getCoreApiUrl()}/api/conversation?limit=1`, {\n headers: coreHeaders(),\n });\n\n if (res.ok) {\n const data = await res.json();\n const items = Array.isArray(data.items) ? data.items : [];\n const turn = items[0];\n if (turn?.assistant || turn?.turn?.assistant) {\n const assistant = turn.assistant || turn.turn?.assistant;\n const text = blocksToText(assistant.blocks);\n const actions: UIAction[] | undefined =\n Array.isArray(assistant.actions) && assistant.actions.length\n ? assistant.actions\n : undefined;\n if (text) {\n updateJobProgress(jobId, {\n status,\n resultMessage: text,\n ...(actions ? { resultActions: actions } : {}),\n });\n return;\n }\n }\n }\n } catch {}\n await new Promise(r => setTimeout(r, 800));\n }\n\n // Fallback: extract from job snapshot\n const fallback =\n jobSnap.result?.message ||\n jobSnap.state?.finalReply ||\n (status === 'error' ? 'Something went wrong. Please try again.' : 'Done.');\n\n updateJobProgress(jobId, { status, resultMessage: fallback });\n}\n\n// ---------------------------------------------------------------------------\n// Polling fallback (when WS is unavailable)\n// ---------------------------------------------------------------------------\n\nasync function pollJob(jobId: string, token: string | null) {\n const { updateJobProgress } = useChatStore.getState();\n\n for (let i = 0; i < 120; i++) {\n try {\n const res = await fetch(`${getCoreApiUrl()}/api/job/${jobId}/status`, {\n headers: {\n ...(token ? { Authorization: `Bearer ${token}` } : {}),\n },\n });\n\n if (!res.ok) {\n if (res.status === 404) return;\n continue;\n }\n\n const snap = await res.json();\n const progress = snapshotToProgress(snap, jobId);\n\n // sys.askUser — same logic as the WS handler\n if (progress.status === 'paused' && progress.pendingQuestion) {\n updateJobProgress(jobId, progress);\n return; // Stop polling — waiting for user input\n }\n\n updateJobProgress(jobId, progress);\n\n if (isTerminalSnapshot(snap)) {\n fetchTurnResult(jobId, snap);\n return;\n }\n } catch {}\n\n await new Promise(r => setTimeout(r, 1500));\n }\n\n // Timeout\n updateJobProgress(jobId, {\n status: 'error',\n resultMessage: 'Job timed out. Please try again.',\n });\n}\n","/**\n * Lightweight event emitter bridging the chat job stream → plugin bridges\n * (React Native has no Node 'events' module).\n *\n * Events: 'plugin-command' (raw command), 'focus-plugin' ({ pluginId, props }),\n * 'plugin-close' ({ pluginId }), 'prepare-chat-message' ({ text }).\n */\n\ntype Listener = (data: any) => void;\n\nexport interface BufferedCommand {\n pluginId: string;\n command: string;\n args?: any;\n correlationId?: string;\n _bufferedAt: number;\n}\n\nconst RECENT_TTL_MS = 10000;\nconst RECENT_MAX = 20;\n\nexport class PluginCommandEmitter {\n private listeners = new Map<string, Set<Listener>>();\n // Ring buffer of plugin commands seen recently. Lets a plugin that mounts\n // AFTER its first command was emitted (e.g. a surface mounted by the\n // dispatcher in response to focus-plugin) still pick that command up via\n // `drainPluginCommands(pluginId)`. Self-cleans on TTL + size cap.\n private recent: BufferedCommand[] = [];\n\n on(event: string, fn: Listener): () => void {\n if (!this.listeners.has(event)) this.listeners.set(event, new Set());\n this.listeners.get(event)!.add(fn);\n return () => this.off(event, fn);\n }\n\n off(event: string, fn: Listener): void {\n this.listeners.get(event)?.delete(fn);\n }\n\n emit(event: string, data: any): void {\n if (event === 'plugin-command' && data && data.pluginId && data.command && data.command !== '__focus') {\n this.recent.push({\n pluginId: data.pluginId,\n command: data.command,\n args: data.args,\n correlationId: data.correlationId,\n _bufferedAt: Date.now(),\n });\n if (this.recent.length > RECENT_MAX) this.recent.shift();\n }\n this.listeners.get(event)?.forEach((fn) => {\n try { fn(data); } catch {}\n });\n }\n\n /** Pop and return any plugin-commands targeted at `pluginId` that were\n * emitted within the last RECENT_TTL_MS. Used by the native bridge on\n * mount so the surface-dispatcher path doesn't drop the first command. */\n drainPluginCommands(pluginId: string): BufferedCommand[] {\n const now = Date.now();\n const out: BufferedCommand[] = [];\n const remaining: BufferedCommand[] = [];\n for (const cmd of this.recent) {\n if (now - cmd._bufferedAt > RECENT_TTL_MS) continue; // expired, drop\n if (cmd.pluginId === pluginId) out.push(cmd);\n else remaining.push(cmd);\n }\n this.recent = remaining;\n return out;\n }\n}\n\n/** App-wide bus for plugin commands arriving over the chat stream. */\nexport const pluginCommandEmitter = new PluginCommandEmitter();\n","// ADAS design tokens — shared by @ateam-ai-mobile/ui and host apps that want to match.\n\nexport const colors = {\n dark: {\n bgPrimary: '#0a0e14',\n bgSecondary: '#111820',\n bgTertiary: '#1a2230',\n bgInput: '#0d1219',\n bgHover: '#1e2a3a',\n border: 'rgba(255, 255, 255, 0.08)',\n borderHover: 'rgba(255, 255, 255, 0.15)',\n textPrimary: '#e8ecf1',\n textSecondary: '#8b95a5',\n textMuted: '#5a6577',\n accent: '#2563eb',\n accentHover: '#3b82f6',\n accentSoft: 'rgba(37, 99, 235, 0.15)',\n green: '#16a34a',\n greenSoft: 'rgba(22, 163, 74, 0.15)',\n red: '#dc2626',\n redSoft: 'rgba(220, 38, 38, 0.15)',\n amber: '#f59e0b',\n amberSoft: 'rgba(245, 158, 11, 0.15)',\n purple: '#8b5cf6',\n purpleSoft: 'rgba(139, 92, 246, 0.15)',\n // Message-specific\n userBubble: '#2563eb',\n botBubble: '#1a2230',\n botBubbleBorder: 'rgba(255, 255, 255, 0.08)',\n },\n light: {\n bgPrimary: '#ffffff',\n bgSecondary: '#f8f9fa',\n bgTertiary: '#f1f3f5',\n bgInput: '#ffffff',\n bgHover: '#e9ecef',\n border: 'rgba(0, 0, 0, 0.08)',\n borderHover: 'rgba(0, 0, 0, 0.15)',\n textPrimary: '#1a1a2e',\n textSecondary: '#6b7280',\n textMuted: '#9ca3af',\n accent: '#2563eb',\n accentHover: '#1d4ed8',\n accentSoft: 'rgba(37, 99, 235, 0.1)',\n green: '#16a34a',\n greenSoft: 'rgba(22, 163, 74, 0.1)',\n red: '#dc2626',\n redSoft: 'rgba(220, 38, 38, 0.1)',\n amber: '#f59e0b',\n amberSoft: 'rgba(245, 158, 11, 0.1)',\n purple: '#8b5cf6',\n purpleSoft: 'rgba(139, 92, 246, 0.1)',\n userBubble: '#2563eb',\n botBubble: '#f1f3f5',\n botBubbleBorder: 'rgba(0, 0, 0, 0.08)',\n },\n} as const;\n\n// Ada palette — warm, paper, personal. Used by the consumer chat experience.\nexport const ada = {\n paper: '#FAF6EE',\n paperSoft: '#F2EADC',\n paperDeep: '#EBE0CC',\n ink: '#1a1410',\n inkSoft: '#5a4d3f',\n inkMuted: '#9a8870',\n border: 'rgba(26, 20, 16, 0.08)',\n borderStrong: 'rgba(26, 20, 16, 0.16)',\n orange: '#FF7A28',\n orangeDeep: '#B8410A',\n orangeSoft: 'rgba(255, 122, 40, 0.14)',\n userBubble: '#1a1410',\n userBubbleText: '#FAF6EE',\n botBubble: '#F2EADC',\n botBubbleBorder: 'rgba(26, 20, 16, 0.06)',\n botBubbleText: '#1a1410',\n} as const;\n\nexport const spacing = {\n xs: 4,\n sm: 8,\n md: 12,\n lg: 16,\n xl: 20,\n xxl: 24,\n xxxl: 32,\n} as const;\n\nexport const fontSize = {\n xs: 11,\n sm: 13,\n md: 15,\n lg: 17,\n xl: 20,\n xxl: 24,\n title: 28,\n} as const;\n\nexport const borderRadius = {\n sm: 8,\n md: 12,\n lg: 16,\n xl: 20,\n full: 9999,\n} as const;\n"],"mappings":"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","names":["text"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ateam-ai-mobile/core",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Headless ADAS SDK core \u2014 auth bridge, API client, chat/job streaming, MCP calls, theme tokens. No UI, no Expo dependencies.",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"typecheck": "tsc --noEmit",
|
|
9
|
+
"build": "tsup"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"zustand": "^5.0.0"
|
|
13
|
+
},
|
|
14
|
+
"peerDependencies": {
|
|
15
|
+
"react": ">=18"
|
|
16
|
+
},
|
|
17
|
+
"module": "dist/index.mjs",
|
|
18
|
+
"source": "src/index.ts",
|
|
19
|
+
"react-native": "src/index.ts",
|
|
20
|
+
"files": [
|
|
21
|
+
"dist",
|
|
22
|
+
"src"
|
|
23
|
+
],
|
|
24
|
+
"publishConfig": {
|
|
25
|
+
"access": "public"
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { getCoreApiUrl, getVoiceApiUrl, getToken, refreshToken, notifyAuthFailure } from '../config';
|
|
2
|
+
|
|
3
|
+
type FetchOptions = RequestInit & {
|
|
4
|
+
baseUrl?: 'core' | 'voice';
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Base API client with JWT injection and 401 refresh handling.
|
|
9
|
+
*
|
|
10
|
+
* Auth failure semantics (preserved from the reference app): a transient
|
|
11
|
+
* 5xx or network blip on the RETRY after a successful refresh must NOT be
|
|
12
|
+
* treated as sign-out. notifyAuthFailure() fires only when auth is genuinely
|
|
13
|
+
* broken:
|
|
14
|
+
* - Refresh itself was rejected with 401/403 → auth failure
|
|
15
|
+
* - Retry with the fresh JWT still returns 401/403 → auth failure
|
|
16
|
+
* Anything else throws but keeps the session intact — the caller may retry
|
|
17
|
+
* on the next user action.
|
|
18
|
+
*/
|
|
19
|
+
export async function apiFetch<T = unknown>(
|
|
20
|
+
path: string,
|
|
21
|
+
options: FetchOptions = {},
|
|
22
|
+
): Promise<T> {
|
|
23
|
+
const { baseUrl = 'core', ...fetchOptions } = options;
|
|
24
|
+
const base = baseUrl === 'voice' ? getVoiceApiUrl() : getCoreApiUrl();
|
|
25
|
+
const url = `${base}${path}`;
|
|
26
|
+
|
|
27
|
+
const token = getToken();
|
|
28
|
+
|
|
29
|
+
const headers: Record<string, string> = {
|
|
30
|
+
...(fetchOptions.headers as Record<string, string>),
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
if (token && !headers['Authorization']) {
|
|
34
|
+
headers['Authorization'] = `Bearer ${token}`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Note: we intentionally DO NOT send a tenant header. The backend derives
|
|
38
|
+
// the tenant from the JWT — a client-supplied tenant paired with a leaked
|
|
39
|
+
// JWT is a tenant-pivot footgun. Tenant comes from the token, not the client.
|
|
40
|
+
|
|
41
|
+
if (!headers['Content-Type'] && fetchOptions.body && typeof fetchOptions.body === 'string') {
|
|
42
|
+
headers['Content-Type'] = 'application/json';
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const res = await fetch(url, { ...fetchOptions, headers });
|
|
46
|
+
|
|
47
|
+
if (res.status === 401 && token) {
|
|
48
|
+
console.warn(`[adas] 401 on ${path} — attempting refresh`);
|
|
49
|
+
const outcome = await refreshToken();
|
|
50
|
+
if (outcome.ok === 'ok') {
|
|
51
|
+
const newToken = getToken();
|
|
52
|
+
headers['Authorization'] = `Bearer ${newToken}`;
|
|
53
|
+
const retryRes = await fetch(url, { ...fetchOptions, headers });
|
|
54
|
+
if (retryRes.ok) {
|
|
55
|
+
return retryRes.json() as Promise<T>;
|
|
56
|
+
}
|
|
57
|
+
if (retryRes.status === 401 || retryRes.status === 403) {
|
|
58
|
+
// Still auth-broken with a fresh JWT — genuinely dead session.
|
|
59
|
+
console.warn(`[adas] retry after refresh still ${retryRes.status} on ${path} — auth failure`);
|
|
60
|
+
notifyAuthFailure();
|
|
61
|
+
throw new Error('Session expired');
|
|
62
|
+
}
|
|
63
|
+
// Non-auth error on retry. Surface it, but the session is fine.
|
|
64
|
+
const text = await retryRes.text().catch(() => '');
|
|
65
|
+
throw new Error(`API ${retryRes.status}: ${text}`);
|
|
66
|
+
}
|
|
67
|
+
if (outcome.ok === 'auth_failed') {
|
|
68
|
+
console.warn(`[adas] refresh auth_failed on ${path} (${outcome.reason}) — auth failure`);
|
|
69
|
+
notifyAuthFailure();
|
|
70
|
+
throw new Error('Session expired');
|
|
71
|
+
}
|
|
72
|
+
// 'transient' — network/5xx on refresh. Keep the session; surface the 401.
|
|
73
|
+
throw new Error(`API 401: refresh transient (${outcome.reason})`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (!res.ok) {
|
|
77
|
+
const text = await res.text();
|
|
78
|
+
throw new Error(`API ${res.status}: ${text}`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return res.json() as Promise<T>;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Fetch with raw response (non-JSON responses, streaming bodies).
|
|
86
|
+
*/
|
|
87
|
+
export async function apiFetchRaw(
|
|
88
|
+
path: string,
|
|
89
|
+
options: FetchOptions = {},
|
|
90
|
+
): Promise<Response> {
|
|
91
|
+
const { baseUrl = 'core', ...fetchOptions } = options;
|
|
92
|
+
const base = baseUrl === 'voice' ? getVoiceApiUrl() : getCoreApiUrl();
|
|
93
|
+
const url = `${base}${path}`;
|
|
94
|
+
|
|
95
|
+
const token = getToken();
|
|
96
|
+
const headers: Record<string, string> = {
|
|
97
|
+
...(fetchOptions.headers as Record<string, string>),
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
if (token && !headers['Authorization']) {
|
|
101
|
+
headers['Authorization'] = `Bearer ${token}`;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return fetch(url, { ...fetchOptions, headers });
|
|
105
|
+
}
|
package/src/api/mcp.ts
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared MCP client — single API layer for ALL plugin communication.
|
|
3
|
+
*
|
|
4
|
+
* Two call patterns:
|
|
5
|
+
* 1. connectorCall(connectorId, tool, args) → POST /api/connectors/:id/call
|
|
6
|
+
* 2. mcpFetch(body) → POST /mcp (raw JSON-RPC passthrough)
|
|
7
|
+
*/
|
|
8
|
+
import { apiFetchRaw } from './client';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Call a tool on a specific connector via POST /api/connectors/:id/call.
|
|
12
|
+
*
|
|
13
|
+
* NOTE: intentionally bypasses the /mcp proxy (which routes to adas-mcp).
|
|
14
|
+
* adas-mcp only knows skill-generated tools, NOT raw connector tools like
|
|
15
|
+
* triggers.list — the direct connector API is the reliable path.
|
|
16
|
+
*/
|
|
17
|
+
export async function connectorCall(
|
|
18
|
+
connectorId: string,
|
|
19
|
+
tool: string,
|
|
20
|
+
args: Record<string, any> = {},
|
|
21
|
+
): Promise<{ ok: boolean; result: any; error?: string }> {
|
|
22
|
+
try {
|
|
23
|
+
const res = await apiFetchRaw(
|
|
24
|
+
`/api/connectors/${encodeURIComponent(connectorId)}/call`,
|
|
25
|
+
{
|
|
26
|
+
method: 'POST',
|
|
27
|
+
headers: { 'Content-Type': 'application/json' },
|
|
28
|
+
body: JSON.stringify({ tool, args }),
|
|
29
|
+
},
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
if (!res.ok) {
|
|
33
|
+
const text = await res.text();
|
|
34
|
+
return { ok: false, result: null, error: `HTTP ${res.status}: ${text}` };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const data = await res.json();
|
|
38
|
+
// Normalize: Core may return { ok, result } or raw MCP { content: [...] }
|
|
39
|
+
if (data.ok !== undefined) return data;
|
|
40
|
+
return { ok: true, result: data };
|
|
41
|
+
} catch (err) {
|
|
42
|
+
return { ok: false, result: null, error: err instanceof Error ? err.message : String(err) };
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Raw JSON-RPC passthrough to /mcp. Returns the full JSON-RPC response. */
|
|
47
|
+
export async function mcpFetch(body: any): Promise<any> {
|
|
48
|
+
const res = await apiFetchRaw('/mcp', {
|
|
49
|
+
method: 'POST',
|
|
50
|
+
headers: { 'Content-Type': 'application/json' },
|
|
51
|
+
body: typeof body === 'string' ? body : JSON.stringify(body),
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
if (!res.ok) {
|
|
55
|
+
const text = await res.text();
|
|
56
|
+
throw new Error(`MCP fetch failed: HTTP ${res.status}: ${text}`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return res.json();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** JSON-RPC tools/call to /mcp (for core tools like cp.fe_api). */
|
|
63
|
+
export async function mcpToolCall(
|
|
64
|
+
toolName: string,
|
|
65
|
+
args: Record<string, any> = {},
|
|
66
|
+
): Promise<any> {
|
|
67
|
+
return mcpFetch({
|
|
68
|
+
jsonrpc: '2.0',
|
|
69
|
+
id: Date.now(),
|
|
70
|
+
method: 'tools/call',
|
|
71
|
+
params: { name: toolName, arguments: args },
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Post a plugin command result back to Core. */
|
|
76
|
+
export async function postCommandResult(
|
|
77
|
+
correlationId: string,
|
|
78
|
+
result: any,
|
|
79
|
+
error?: string,
|
|
80
|
+
): Promise<void> {
|
|
81
|
+
await apiFetchRaw('/api/plugin-command-result', {
|
|
82
|
+
method: 'POST',
|
|
83
|
+
headers: { 'Content-Type': 'application/json' },
|
|
84
|
+
body: JSON.stringify({ correlationId, result, error }),
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Unwrap MCP JSON-RPC text content → parsed business data.
|
|
90
|
+
* Server returns: { content: [{ type: "text", text: "{...}" }] }
|
|
91
|
+
*/
|
|
92
|
+
export function unwrapMcpResult(result: any): any {
|
|
93
|
+
if (!result) return result;
|
|
94
|
+
if (!result.content) return result;
|
|
95
|
+
const textItem = Array.isArray(result.content)
|
|
96
|
+
? result.content.find((c: any) => c.type === 'text')
|
|
97
|
+
: null;
|
|
98
|
+
if (!textItem?.text) return result;
|
|
99
|
+
try {
|
|
100
|
+
return JSON.parse(textItem.text);
|
|
101
|
+
} catch {
|
|
102
|
+
return textItem.text;
|
|
103
|
+
}
|
|
104
|
+
}
|