@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.
@@ -0,0 +1,76 @@
1
+ import type { AttachmentPreview } from '../storage/types';
2
+
3
+ export interface ToolCall {
4
+ name: string;
5
+ args: Record<string, unknown>;
6
+ result: unknown;
7
+ skill?: string;
8
+ }
9
+
10
+ export interface JobProgress {
11
+ jobId: string;
12
+ /** Actor that owns the running chain — needed when restoring active work. */
13
+ actorId?: string;
14
+ status: 'running' | 'done' | 'error' | 'paused' | 'unknown';
15
+ subStatus?: string;
16
+ phase?: string;
17
+ iteration?: number;
18
+ /** Active skill slug at this point in the chain (e.g. "messaging-agent") */
19
+ skillSlug?: string;
20
+ /** Most recent tool being invoked by the active skill */
21
+ currentTool?: string;
22
+ /** Wall-clock timestamp when the chain started — used for elapsed timer */
23
+ chainStartedAt?: number;
24
+ resultMessage?: string;
25
+ /** UI Action chips to attach to the assistant bubble when this job finishes. */
26
+ resultActions?: UIAction[];
27
+ /** Live progress from adas_emit_progress() — latest event from a running fat tool */
28
+ liveProgress?: { message: string; step?: number; total?: number };
29
+ pendingQuestion?: {
30
+ inputId: string;
31
+ question: string;
32
+ inputType: 'text' | 'choice' | 'confirm';
33
+ options?: string[];
34
+ placeholder?: string;
35
+ };
36
+ }
37
+
38
+ export type MessageAttachment = AttachmentPreview;
39
+
40
+ /**
41
+ * UI Action chip — emitted by the polisher LLM, validated by cp.uiActions_api.
42
+ */
43
+ export interface UIAction {
44
+ intent: 'view_entity' | 'suggested_action' | 'quick_toggle' | 'cross_plugin';
45
+ label: string;
46
+ description?: string;
47
+ icon?: string;
48
+ /** Short plugin id (e.g. "triggers-panel") — what the LLM picks from the inventory. */
49
+ target: string;
50
+ /** Full plugin id — added by validator, used by the host to navigate. */
51
+ pluginId?: string;
52
+ /** Already-substituted iframe URL — used by web; mobile ignores it. */
53
+ resolvedUrl?: string;
54
+ /** Intent-specific payload. */
55
+ payload: Record<string, any>;
56
+ }
57
+
58
+ export interface Message {
59
+ id: string;
60
+ role: 'user' | 'assistant' | 'system';
61
+ content: string;
62
+ timestamp: Date;
63
+ toolCalls?: ToolCall[];
64
+ jobProgress?: JobProgress;
65
+ /** Skill that handled this turn */
66
+ skillSlug?: string;
67
+ /** Attached images/files */
68
+ attachments?: MessageAttachment[];
69
+ /** UI Action chips beneath assistant bubbles */
70
+ actions?: UIAction[];
71
+ /** Chain id from /api/conversation — used to attach live progress to the
72
+ * correct persisted turn instead of fabricating a duplicate bubble. */
73
+ chainId?: string;
74
+ /** Turn variant. 'notification' = proactive/system-initiated. */
75
+ kind?: 'notification' | string;
76
+ }
package/src/config.ts ADDED
@@ -0,0 +1,118 @@
1
+ /**
2
+ * SDK configuration — the single injection point between the host app and
3
+ * the ADAS core. The host owns authentication, identity, and URL discovery;
4
+ * the SDK reads everything through the config registered via initAdas().
5
+ *
6
+ * There is intentionally ONE active config per JS context (module singleton):
7
+ * the chat store, WebSocket, and API client below it are app-wide singletons
8
+ * too, and a second tenant/backend inside one app is not a supported v1 case.
9
+ */
10
+
11
+ import type { AttachmentCache } from './storage/types';
12
+
13
+ /** Result of a host-driven token refresh. Mirrors the semantics the API
14
+ * client depends on: only 'auth_failed' logs the user out; 'transient'
15
+ * (network blip, 5xx) keeps the session so the next call can retry. */
16
+ export interface RefreshOutcome {
17
+ ok: 'ok' | 'auth_failed' | 'transient';
18
+ status: number;
19
+ reason?: string;
20
+ }
21
+
22
+ /**
23
+ * Host-implemented auth bridge. The SDK never shows login UI and never
24
+ * stores credentials — the host authenticates its own users and hands the
25
+ * SDK a way to read/refresh the ADAS JWT it minted for them (via the
26
+ * server-to-server token exchange).
27
+ */
28
+ export interface TokenProvider {
29
+ /** Current ADAS JWT, or null when signed out. Called on every request —
30
+ * keep it synchronous and cheap (read from memory, not disk). */
31
+ getToken(): string | null;
32
+ /** Called on 401. Host should mint/refresh the ADAS token and return the
33
+ * outcome; 'ok' makes the SDK retry the failed request once. */
34
+ refresh(): Promise<RefreshOutcome>;
35
+ /** Auth is genuinely dead (refresh rejected, or retry still 401). Host
36
+ * should treat this as sign-out for the embedded experience. */
37
+ onAuthFailure?: () => void;
38
+ }
39
+
40
+ export interface AdasUser {
41
+ actorId: string;
42
+ name?: string;
43
+ email?: string;
44
+ }
45
+
46
+ export interface AdasConfig {
47
+ /** Core API base URL, e.g. https://app.ateam-ai.com */
48
+ coreUrl: string;
49
+ /** Voice service base URL (optional — only voice features need it). */
50
+ voiceUrl?: string;
51
+ tokenProvider: TokenProvider;
52
+ /** Tenant slug — informational/telemetry only. The backend derives the
53
+ * tenant from the JWT; the SDK never sends a tenant header. */
54
+ tenant?: string;
55
+ /** Current end user as known to the host. actorId is used as the chat
56
+ * reply context; without it, replies route to 'unknown'. */
57
+ getUser?: () => AdasUser | null;
58
+ /** Optional persistence for outgoing image/file attachments so previews
59
+ * survive app restarts. No-op when omitted (previews live in memory for
60
+ * the session only). @ateam-ai-mobile/ui ships an Expo/RN implementation. */
61
+ attachmentCache?: AttachmentCache;
62
+ }
63
+
64
+ let _config: AdasConfig | null = null;
65
+
66
+ /** Register the SDK configuration. Call once at app start, before any other
67
+ * SDK API. Calling again replaces the config (e.g. env switch) — combine
68
+ * with chat.clearChat() and closeChatStream() for a clean swap. */
69
+ export function initAdas(config: AdasConfig): void {
70
+ _config = config;
71
+ }
72
+
73
+ export function isAdasInitialized(): boolean {
74
+ return _config !== null;
75
+ }
76
+
77
+ export function getConfig(): AdasConfig {
78
+ if (!_config) {
79
+ throw new Error('[adas] SDK not initialized — call initAdas(config) before using any ADAS API');
80
+ }
81
+ return _config;
82
+ }
83
+
84
+ export function getCoreApiUrl(): string {
85
+ return getConfig().coreUrl.replace(/\/$/, '');
86
+ }
87
+
88
+ export function getCoreWsUrl(): string {
89
+ return getCoreApiUrl().replace(/^http/, 'ws');
90
+ }
91
+
92
+ export function getVoiceApiUrl(): string {
93
+ const url = getConfig().voiceUrl;
94
+ if (!url) throw new Error('[adas] voiceUrl not configured');
95
+ return url.replace(/\/$/, '');
96
+ }
97
+
98
+ export function getTenant(): string {
99
+ return getConfig().tenant || '';
100
+ }
101
+
102
+ export function getUser(): AdasUser | null {
103
+ return getConfig().getUser?.() || null;
104
+ }
105
+
106
+ // ─── Internal token helpers (thin wrappers over the host's TokenProvider) ──
107
+
108
+ export function getToken(): string | null {
109
+ return getConfig().tokenProvider.getToken();
110
+ }
111
+
112
+ export async function refreshToken(): Promise<RefreshOutcome> {
113
+ return getConfig().tokenProvider.refresh();
114
+ }
115
+
116
+ export function notifyAuthFailure(): void {
117
+ getConfig().tokenProvider.onAuthFailure?.();
118
+ }
package/src/index.ts ADDED
@@ -0,0 +1,72 @@
1
+ /**
2
+ * @ateam-ai-mobile/core — headless ADAS SDK for React Native.
3
+ *
4
+ * The host app owns authentication and navigation; this package provides
5
+ * everything between the ADAS backend and the UI: config/auth bridge, API
6
+ * client, chat store with live job streaming, MCP tool calls, and theme
7
+ * tokens. No Expo dependencies — works in Expo and bare RN apps alike.
8
+ *
9
+ * Quick start:
10
+ * initAdas({
11
+ * coreUrl: 'https://app.ateam-ai.com',
12
+ * tokenProvider: { getToken, refresh, onAuthFailure },
13
+ * getUser: () => ({ actorId: currentUser.id }),
14
+ * });
15
+ * const { messages, sendMessage } = useChatStore();
16
+ */
17
+
18
+ // Config & auth bridge
19
+ export {
20
+ initAdas,
21
+ isAdasInitialized,
22
+ getConfig,
23
+ getCoreApiUrl,
24
+ getCoreWsUrl,
25
+ getVoiceApiUrl,
26
+ getTenant,
27
+ getUser,
28
+ getToken,
29
+ } from './config';
30
+ export type { AdasConfig, TokenProvider, RefreshOutcome, AdasUser } from './config';
31
+
32
+ // API client
33
+ export { apiFetch, apiFetchRaw } from './api/client';
34
+
35
+ // MCP / connector calls
36
+ export {
37
+ connectorCall,
38
+ mcpFetch,
39
+ mcpToolCall,
40
+ postCommandResult,
41
+ unwrapMcpResult,
42
+ } from './api/mcp';
43
+
44
+ // Chat
45
+ export {
46
+ useChatStore,
47
+ closeChatStream,
48
+ blocksToText,
49
+ TABLE_BLOCK_OPEN,
50
+ TABLE_BLOCK_CLOSE,
51
+ } from './chat/store';
52
+ export type {
53
+ Message,
54
+ MessageAttachment,
55
+ JobProgress,
56
+ UIAction,
57
+ ToolCall,
58
+ } from './chat/types';
59
+
60
+ // Storage adapter contracts
61
+ export type {
62
+ AttachmentCache,
63
+ AttachmentPreview,
64
+ OutgoingAttachment,
65
+ } from './storage/types';
66
+
67
+ // Plugin command bus
68
+ export { pluginCommandEmitter, PluginCommandEmitter } from './utils/emitter';
69
+ export type { BufferedCommand } from './utils/emitter';
70
+
71
+ // Theme tokens
72
+ export { colors, ada, spacing, fontSize, borderRadius } from './theme/tokens';
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Storage adapter contracts. The core is platform-agnostic (no Expo, no
3
+ * AsyncStorage imports) — hosts or @ateam-ai-mobile/ui plug in concrete implementations.
4
+ */
5
+
6
+ /** Outgoing attachment as sent to /api/chat (base64 payload). */
7
+ export interface OutgoingAttachment {
8
+ data: string;
9
+ mimeType: string;
10
+ name: string;
11
+ }
12
+
13
+ /** Attachment as rendered in a message bubble (local or remote preview URI). */
14
+ export interface AttachmentPreview {
15
+ mimeType: string;
16
+ name: string;
17
+ previewUri?: string;
18
+ }
19
+
20
+ /**
21
+ * Persists outgoing attachments keyed by jobId so image previews survive
22
+ * app restarts (/api/conversation does not echo user attachments back).
23
+ */
24
+ export interface AttachmentCache {
25
+ /** Resolve once the index is loaded — get() before this may return undefined. */
26
+ ready(): Promise<void>;
27
+ save(jobId: string, attachments: OutgoingAttachment[]): Promise<void>;
28
+ get(jobId: string): AttachmentPreview[] | undefined;
29
+ }
@@ -0,0 +1,105 @@
1
+ // ADAS design tokens — shared by @ateam-ai-mobile/ui and host apps that want to match.
2
+
3
+ export const colors = {
4
+ dark: {
5
+ bgPrimary: '#0a0e14',
6
+ bgSecondary: '#111820',
7
+ bgTertiary: '#1a2230',
8
+ bgInput: '#0d1219',
9
+ bgHover: '#1e2a3a',
10
+ border: 'rgba(255, 255, 255, 0.08)',
11
+ borderHover: 'rgba(255, 255, 255, 0.15)',
12
+ textPrimary: '#e8ecf1',
13
+ textSecondary: '#8b95a5',
14
+ textMuted: '#5a6577',
15
+ accent: '#2563eb',
16
+ accentHover: '#3b82f6',
17
+ accentSoft: 'rgba(37, 99, 235, 0.15)',
18
+ green: '#16a34a',
19
+ greenSoft: 'rgba(22, 163, 74, 0.15)',
20
+ red: '#dc2626',
21
+ redSoft: 'rgba(220, 38, 38, 0.15)',
22
+ amber: '#f59e0b',
23
+ amberSoft: 'rgba(245, 158, 11, 0.15)',
24
+ purple: '#8b5cf6',
25
+ purpleSoft: 'rgba(139, 92, 246, 0.15)',
26
+ // Message-specific
27
+ userBubble: '#2563eb',
28
+ botBubble: '#1a2230',
29
+ botBubbleBorder: 'rgba(255, 255, 255, 0.08)',
30
+ },
31
+ light: {
32
+ bgPrimary: '#ffffff',
33
+ bgSecondary: '#f8f9fa',
34
+ bgTertiary: '#f1f3f5',
35
+ bgInput: '#ffffff',
36
+ bgHover: '#e9ecef',
37
+ border: 'rgba(0, 0, 0, 0.08)',
38
+ borderHover: 'rgba(0, 0, 0, 0.15)',
39
+ textPrimary: '#1a1a2e',
40
+ textSecondary: '#6b7280',
41
+ textMuted: '#9ca3af',
42
+ accent: '#2563eb',
43
+ accentHover: '#1d4ed8',
44
+ accentSoft: 'rgba(37, 99, 235, 0.1)',
45
+ green: '#16a34a',
46
+ greenSoft: 'rgba(22, 163, 74, 0.1)',
47
+ red: '#dc2626',
48
+ redSoft: 'rgba(220, 38, 38, 0.1)',
49
+ amber: '#f59e0b',
50
+ amberSoft: 'rgba(245, 158, 11, 0.1)',
51
+ purple: '#8b5cf6',
52
+ purpleSoft: 'rgba(139, 92, 246, 0.1)',
53
+ userBubble: '#2563eb',
54
+ botBubble: '#f1f3f5',
55
+ botBubbleBorder: 'rgba(0, 0, 0, 0.08)',
56
+ },
57
+ } as const;
58
+
59
+ // Ada palette — warm, paper, personal. Used by the consumer chat experience.
60
+ export const ada = {
61
+ paper: '#FAF6EE',
62
+ paperSoft: '#F2EADC',
63
+ paperDeep: '#EBE0CC',
64
+ ink: '#1a1410',
65
+ inkSoft: '#5a4d3f',
66
+ inkMuted: '#9a8870',
67
+ border: 'rgba(26, 20, 16, 0.08)',
68
+ borderStrong: 'rgba(26, 20, 16, 0.16)',
69
+ orange: '#FF7A28',
70
+ orangeDeep: '#B8410A',
71
+ orangeSoft: 'rgba(255, 122, 40, 0.14)',
72
+ userBubble: '#1a1410',
73
+ userBubbleText: '#FAF6EE',
74
+ botBubble: '#F2EADC',
75
+ botBubbleBorder: 'rgba(26, 20, 16, 0.06)',
76
+ botBubbleText: '#1a1410',
77
+ } as const;
78
+
79
+ export const spacing = {
80
+ xs: 4,
81
+ sm: 8,
82
+ md: 12,
83
+ lg: 16,
84
+ xl: 20,
85
+ xxl: 24,
86
+ xxxl: 32,
87
+ } as const;
88
+
89
+ export const fontSize = {
90
+ xs: 11,
91
+ sm: 13,
92
+ md: 15,
93
+ lg: 17,
94
+ xl: 20,
95
+ xxl: 24,
96
+ title: 28,
97
+ } as const;
98
+
99
+ export const borderRadius = {
100
+ sm: 8,
101
+ md: 12,
102
+ lg: 16,
103
+ xl: 20,
104
+ full: 9999,
105
+ } as const;
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Lightweight event emitter bridging the chat job stream → plugin bridges
3
+ * (React Native has no Node 'events' module).
4
+ *
5
+ * Events: 'plugin-command' (raw command), 'focus-plugin' ({ pluginId, props }),
6
+ * 'plugin-close' ({ pluginId }), 'prepare-chat-message' ({ text }).
7
+ */
8
+
9
+ type Listener = (data: any) => void;
10
+
11
+ export interface BufferedCommand {
12
+ pluginId: string;
13
+ command: string;
14
+ args?: any;
15
+ correlationId?: string;
16
+ _bufferedAt: number;
17
+ }
18
+
19
+ const RECENT_TTL_MS = 10000;
20
+ const RECENT_MAX = 20;
21
+
22
+ export class PluginCommandEmitter {
23
+ private listeners = new Map<string, Set<Listener>>();
24
+ // Ring buffer of plugin commands seen recently. Lets a plugin that mounts
25
+ // AFTER its first command was emitted (e.g. a surface mounted by the
26
+ // dispatcher in response to focus-plugin) still pick that command up via
27
+ // `drainPluginCommands(pluginId)`. Self-cleans on TTL + size cap.
28
+ private recent: BufferedCommand[] = [];
29
+
30
+ on(event: string, fn: Listener): () => void {
31
+ if (!this.listeners.has(event)) this.listeners.set(event, new Set());
32
+ this.listeners.get(event)!.add(fn);
33
+ return () => this.off(event, fn);
34
+ }
35
+
36
+ off(event: string, fn: Listener): void {
37
+ this.listeners.get(event)?.delete(fn);
38
+ }
39
+
40
+ emit(event: string, data: any): void {
41
+ if (event === 'plugin-command' && data && data.pluginId && data.command && data.command !== '__focus') {
42
+ this.recent.push({
43
+ pluginId: data.pluginId,
44
+ command: data.command,
45
+ args: data.args,
46
+ correlationId: data.correlationId,
47
+ _bufferedAt: Date.now(),
48
+ });
49
+ if (this.recent.length > RECENT_MAX) this.recent.shift();
50
+ }
51
+ this.listeners.get(event)?.forEach((fn) => {
52
+ try { fn(data); } catch {}
53
+ });
54
+ }
55
+
56
+ /** Pop and return any plugin-commands targeted at `pluginId` that were
57
+ * emitted within the last RECENT_TTL_MS. Used by the native bridge on
58
+ * mount so the surface-dispatcher path doesn't drop the first command. */
59
+ drainPluginCommands(pluginId: string): BufferedCommand[] {
60
+ const now = Date.now();
61
+ const out: BufferedCommand[] = [];
62
+ const remaining: BufferedCommand[] = [];
63
+ for (const cmd of this.recent) {
64
+ if (now - cmd._bufferedAt > RECENT_TTL_MS) continue; // expired, drop
65
+ if (cmd.pluginId === pluginId) out.push(cmd);
66
+ else remaining.push(cmd);
67
+ }
68
+ this.recent = remaining;
69
+ return out;
70
+ }
71
+ }
72
+
73
+ /** App-wide bus for plugin commands arriving over the chat stream. */
74
+ export const pluginCommandEmitter = new PluginCommandEmitter();