multimodel-dev-os 1.0.0 → 2.0.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/.ai/adapters/custom-adapter.example.yaml +9 -0
- package/.ai/adapters/registry.yaml +56 -0
- package/.ai/models/README.md +14 -0
- package/.ai/models/local-models.yaml +20 -0
- package/.ai/models/providers.yaml +29 -0
- package/.ai/models/registry.yaml +73 -0
- package/.ai/models/routing-presets.yaml +23 -0
- package/.ai/skills/custom-skill.example.md +15 -0
- package/.ai/templates/custom-template.example.yaml +19 -0
- package/.ai/templates/registry.yaml +522 -0
- package/README.md +48 -22
- package/assets/favicon.png +0 -0
- package/assets/logo.png +0 -0
- package/bin/multimodel-dev-os.js +810 -91
- package/docs/.vitepress/config.js +69 -0
- package/docs/adapter-authoring.md +46 -0
- package/docs/agent-compatibility.md +51 -0
- package/docs/case-studies/index.md +11 -7
- package/docs/cli-roadmap.md +15 -18
- package/docs/compatibility.md +7 -3
- package/docs/cost-optimization.md +6 -2
- package/docs/faq.md +32 -56
- package/docs/final-launch.md +10 -4
- package/docs/index.md +44 -1
- package/docs/local-models.md +48 -0
- package/docs/mobile-android.md +75 -0
- package/docs/model-compatibility.md +65 -0
- package/docs/model-routing.md +45 -0
- package/docs/npm-publishing.md +27 -0
- package/docs/package-safety.md +29 -0
- package/docs/protocol.md +8 -4
- package/docs/provider-strategy.md +44 -0
- package/docs/public/favicon.png +0 -0
- package/docs/public/humans.txt +13 -0
- package/docs/public/llms-full.txt +82 -0
- package/docs/public/llms.txt +36 -0
- package/docs/public/logo.png +0 -0
- package/docs/public/robots.txt +4 -0
- package/docs/public/sitemap.xml +68 -0
- package/docs/quickstart.md +17 -12
- package/docs/registry-contribution.md +20 -0
- package/docs/release-policy.md +26 -0
- package/docs/skill-authoring.md +56 -0
- package/docs/stable-protocol.md +8 -4
- package/docs/template-authoring.md +65 -0
- package/docs/token-optimization.md +27 -0
- package/docs/v2-migration.md +31 -0
- package/docs/v2-release-checklist.md +30 -0
- package/docs/v2-roadmap.md +95 -0
- package/examples/expo-react-native-android/.ai/config.yaml +22 -0
- package/examples/expo-react-native-android/.ai/context/architecture.md +18 -0
- package/examples/expo-react-native-android/.ai/context/context-budget.md +4 -0
- package/examples/expo-react-native-android/.ai/context/model-map.md +6 -0
- package/examples/expo-react-native-android/.ai/context/project-brief.md +9 -0
- package/examples/expo-react-native-android/.ai/session-logs/.gitkeep +1 -0
- package/examples/expo-react-native-android/.ai/skills/expo-android-build.md +11 -0
- package/examples/expo-react-native-android/AGENTS.md +20 -0
- package/examples/expo-react-native-android/MEMORY.md +13 -0
- package/examples/expo-react-native-android/README.md +101 -0
- package/examples/expo-react-native-android/RUNBOOK.md +36 -0
- package/examples/expo-react-native-android/TASKS.md +14 -0
- package/examples/expo-react-native-android/app.config.ts +40 -0
- package/examples/expo-react-native-android/app.json +34 -0
- package/examples/expo-react-native-android/eas.json +26 -0
- package/examples/expo-react-native-android/jest.config.js +11 -0
- package/examples/expo-react-native-android/src/app/_layout.tsx +89 -0
- package/examples/expo-react-native-android/src/lib/secure-storage.ts +63 -0
- package/examples/expo-react-native-android/src/services/api-client.ts +106 -0
- package/package.json +3 -2
- package/scripts/install.ps1 +230 -230
- package/scripts/install.sh +1 -1
- package/scripts/prepublish-guard.js +43 -0
- package/scripts/verify.js +192 -1
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { ExpoConfig, ConfigContext } from 'expo/config';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Dynamic Expo App Configuration File
|
|
5
|
+
* Decouples staging/production URLs and binds environment variables at build-time.
|
|
6
|
+
*/
|
|
7
|
+
export default ({ config }: ConfigContext): ExpoConfig => {
|
|
8
|
+
const env = process.env.APP_ENV || 'development';
|
|
9
|
+
|
|
10
|
+
const extraConfig = {
|
|
11
|
+
development: {
|
|
12
|
+
apiUrl: 'http://10.0.2.2:3000/api', // Localhost mapping for Android Emulator loopbacks
|
|
13
|
+
envName: 'Development'
|
|
14
|
+
},
|
|
15
|
+
staging: {
|
|
16
|
+
apiUrl: 'https://staging-api.multimodel.dev',
|
|
17
|
+
envName: 'Staging'
|
|
18
|
+
},
|
|
19
|
+
production: {
|
|
20
|
+
apiUrl: 'https://api.multimodel.dev',
|
|
21
|
+
envName: 'Production'
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const selectedEnv = extraConfig[env as keyof typeof extraConfig] || extraConfig.development;
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
...config,
|
|
29
|
+
name: config.name || "MultiModel Dev OS Mobile",
|
|
30
|
+
slug: config.slug || "multimodel-dev-os-mobile",
|
|
31
|
+
// Configure EAS parameters. Fill these in during local setup. Do not commit actual tokens to Git.
|
|
32
|
+
owner: "your-expo-username-placeholder",
|
|
33
|
+
extra: {
|
|
34
|
+
...selectedEnv,
|
|
35
|
+
eas: {
|
|
36
|
+
projectId: "your-eas-project-id-placeholder"
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"expo": {
|
|
3
|
+
"name": "MultiModel Dev OS Mobile",
|
|
4
|
+
"slug": "multimodel-dev-os-mobile",
|
|
5
|
+
"version": "1.0.0",
|
|
6
|
+
"orientation": "portrait",
|
|
7
|
+
"icon": "./assets/icon.png",
|
|
8
|
+
"userInterfaceStyle": "light",
|
|
9
|
+
"splash": {
|
|
10
|
+
"image": "./assets/splash.png",
|
|
11
|
+
"resizeMode": "contain",
|
|
12
|
+
"backgroundColor": "#ffffff"
|
|
13
|
+
},
|
|
14
|
+
"assetBundlePatterns": [
|
|
15
|
+
"**/*"
|
|
16
|
+
],
|
|
17
|
+
"ios": {
|
|
18
|
+
"supportsTablet": true
|
|
19
|
+
},
|
|
20
|
+
"android": {
|
|
21
|
+
"adaptiveIcon": {
|
|
22
|
+
"foregroundImage": "./assets/adaptive-icon.png",
|
|
23
|
+
"backgroundColor": "#ffffff"
|
|
24
|
+
},
|
|
25
|
+
"package": "com.multimodel.devos",
|
|
26
|
+
"permissions": [
|
|
27
|
+
"INTERNET"
|
|
28
|
+
]
|
|
29
|
+
},
|
|
30
|
+
"web": {
|
|
31
|
+
"favicon": "./assets/favicon.png"
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"cli": {
|
|
3
|
+
"version": ">= 9.0.0"
|
|
4
|
+
},
|
|
5
|
+
"build": {
|
|
6
|
+
"development": {
|
|
7
|
+
"developmentClient": true,
|
|
8
|
+
"distribution": "internal",
|
|
9
|
+
"channel": "development"
|
|
10
|
+
},
|
|
11
|
+
"preview": {
|
|
12
|
+
"distribution": "internal",
|
|
13
|
+
"channel": "preview"
|
|
14
|
+
},
|
|
15
|
+
"production": {
|
|
16
|
+
"distribution": "store",
|
|
17
|
+
"channel": "production",
|
|
18
|
+
"android": {
|
|
19
|
+
"buildType": "app-bundle"
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"submit": {
|
|
24
|
+
"production": {}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
preset: 'jest-expo',
|
|
3
|
+
transformIgnorePatterns: [
|
|
4
|
+
'node_modules/(?!((jest-)?react-native|@react-native(-community)?)|expo(nent)?|@expo(nent)?/.*|@expo-google-fonts/.*|react-navigation|@react-navigation/.*|@unimodules/.*|unimodules|sentry-expo|native-base|react-native-svg)'
|
|
5
|
+
],
|
|
6
|
+
setupFilesAfterEnv: ["@testing-library/jest-native/extend-expect"],
|
|
7
|
+
testMatch: ['**/__tests__/**/*.test.[jt]s?(x)', '**/?(*.)+(spec|test).[jt]s?(x)'],
|
|
8
|
+
moduleNameMapper: {
|
|
9
|
+
'^@/(.*)$': '<rootDir>/src/$1'
|
|
10
|
+
}
|
|
11
|
+
};
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import React, { useEffect, useState } from 'react';
|
|
2
|
+
import { StyleSheet, Text, View, ActivityIndicator } from 'react-native';
|
|
3
|
+
import NetInfo from '@react-native-community/netinfo';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Root Application Layout Component
|
|
7
|
+
* Serves as the primary entry point for Expo Router.
|
|
8
|
+
* Resolves network connection states and establishes offline screen boundaries.
|
|
9
|
+
*/
|
|
10
|
+
export default function RootLayout() {
|
|
11
|
+
const [isConnected, setIsConnected] = useState<boolean | null>(true);
|
|
12
|
+
const [isLoading, setIsLoading] = useState<boolean>(true);
|
|
13
|
+
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
// Monitor connection states dynamically across Android devices
|
|
16
|
+
const unsubscribe = NetInfo.addEventListener(state => {
|
|
17
|
+
setIsConnected(state.isConnected);
|
|
18
|
+
setIsLoading(false);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
return () => unsubscribe();
|
|
22
|
+
}, []);
|
|
23
|
+
|
|
24
|
+
if (isLoading) {
|
|
25
|
+
return (
|
|
26
|
+
<View style={styles.center}>
|
|
27
|
+
<ActivityIndicator size="large" color="#6366f1" />
|
|
28
|
+
</View>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Offline boundary gate: Render fallback UI if connection is lost
|
|
33
|
+
if (!isConnected) {
|
|
34
|
+
return (
|
|
35
|
+
<View style={styles.center}>
|
|
36
|
+
<Text style={styles.errorText}>No Internet Connection</Text>
|
|
37
|
+
<Text style={styles.subtext}>Please check your network settings and try again.</Text>
|
|
38
|
+
</View>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Guidelines: For Expo Router navigation, replace the below container with:
|
|
43
|
+
// import { Stack } from 'expo-router';
|
|
44
|
+
// return <Stack screenOptions={{ headerShown: false }} />;
|
|
45
|
+
return (
|
|
46
|
+
<View style={styles.container}>
|
|
47
|
+
<Text style={styles.welcomeTitle}>MultiModel Dev OS Mobile</Text>
|
|
48
|
+
<Text style={styles.body}>Scaffolded React Native App Layout successfully mounted!</Text>
|
|
49
|
+
</View>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const styles = StyleSheet.create({
|
|
54
|
+
container: {
|
|
55
|
+
flex: 1,
|
|
56
|
+
backgroundColor: '#f8fafc',
|
|
57
|
+
alignItems: 'center',
|
|
58
|
+
justifyContent: 'center',
|
|
59
|
+
padding: 24
|
|
60
|
+
},
|
|
61
|
+
center: {
|
|
62
|
+
flex: 1,
|
|
63
|
+
alignItems: 'center',
|
|
64
|
+
justifyContent: 'center',
|
|
65
|
+
padding: 24
|
|
66
|
+
},
|
|
67
|
+
welcomeTitle: {
|
|
68
|
+
fontSize: 24,
|
|
69
|
+
fontWeight: 'bold',
|
|
70
|
+
color: '#0f172a',
|
|
71
|
+
marginBottom: 8
|
|
72
|
+
},
|
|
73
|
+
body: {
|
|
74
|
+
fontSize: 16,
|
|
75
|
+
color: '#475569',
|
|
76
|
+
textAlign: 'center'
|
|
77
|
+
},
|
|
78
|
+
errorText: {
|
|
79
|
+
fontSize: 20,
|
|
80
|
+
fontWeight: 'bold',
|
|
81
|
+
color: '#ef4444',
|
|
82
|
+
marginBottom: 8
|
|
83
|
+
},
|
|
84
|
+
subtext: {
|
|
85
|
+
fontSize: 14,
|
|
86
|
+
color: '#64748b',
|
|
87
|
+
textAlign: 'center'
|
|
88
|
+
}
|
|
89
|
+
});
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import * as SecureStore from 'expo-secure-store';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Encrypted Key-Value Storage Wrapper
|
|
5
|
+
* Safely accesses expo-secure-store and runs assertions to verify key structures.
|
|
6
|
+
*/
|
|
7
|
+
export class SecureStorage {
|
|
8
|
+
static async setItem(key: string, value: string): Promise<boolean> {
|
|
9
|
+
if (!key || key.trim() === '') {
|
|
10
|
+
console.error('SecureStorage: Key cannot be empty');
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
try {
|
|
14
|
+
if (SecureStore && typeof SecureStore.setItemAsync === 'function') {
|
|
15
|
+
await SecureStore.setItemAsync(key, value);
|
|
16
|
+
return true;
|
|
17
|
+
} else {
|
|
18
|
+
console.warn(`[SecureStorage Mock] setItem: ${key} = ${value}`);
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
} catch (e) {
|
|
22
|
+
console.error(`Failed to set item in SecureStore for key: ${key}`, e);
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
static async getItem(key: string): Promise<string | null> {
|
|
28
|
+
if (!key || key.trim() === '') {
|
|
29
|
+
console.error('SecureStorage: Key cannot be empty');
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
try {
|
|
33
|
+
if (SecureStore && typeof SecureStore.getItemAsync === 'function') {
|
|
34
|
+
return await SecureStore.getItemAsync(key);
|
|
35
|
+
} else {
|
|
36
|
+
console.warn(`[SecureStorage Mock] getItem: ${key}`);
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
} catch (e) {
|
|
40
|
+
console.error(`Failed to retrieve item from SecureStore for key: ${key}`, e);
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
static async deleteItem(key: string): Promise<boolean> {
|
|
46
|
+
if (!key || key.trim() === '') {
|
|
47
|
+
console.error('SecureStorage: Key cannot be empty');
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
try {
|
|
51
|
+
if (SecureStore && typeof SecureStore.deleteItemAsync === 'function') {
|
|
52
|
+
await SecureStore.deleteItemAsync(key);
|
|
53
|
+
return true;
|
|
54
|
+
} else {
|
|
55
|
+
console.warn(`[SecureStorage Mock] deleteItem: ${key}`);
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
} catch (e) {
|
|
59
|
+
console.error(`Failed to delete item from SecureStore for key: ${key}`, e);
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import Constants from 'expo-constants';
|
|
2
|
+
|
|
3
|
+
export interface ApiResponse<T> {
|
|
4
|
+
data: T | null;
|
|
5
|
+
error: string | null;
|
|
6
|
+
status: number;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export class ApiClient {
|
|
10
|
+
private baseUrl: string;
|
|
11
|
+
private timeoutMs: number = 10000;
|
|
12
|
+
private maxRetries: number = 3;
|
|
13
|
+
private useMockData: boolean = false; // Toggle true for offline mock validations
|
|
14
|
+
|
|
15
|
+
constructor() {
|
|
16
|
+
// Dynamically retrieve base API URL configured in app.config.ts extras
|
|
17
|
+
const extra = Constants.expoConfig?.extra || {};
|
|
18
|
+
this.baseUrl = extra.apiUrl || 'http://10.0.2.2:3000/api';
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async request<T>(path: string, options: RequestInit = {}): Promise<ApiResponse<T>> {
|
|
22
|
+
if (this.useMockData) {
|
|
23
|
+
// Mock response resolver
|
|
24
|
+
return new Promise((resolve) => {
|
|
25
|
+
setTimeout(() => {
|
|
26
|
+
resolve({
|
|
27
|
+
data: { message: "Mock Success data from ApiClient" } as unknown as T,
|
|
28
|
+
error: null,
|
|
29
|
+
status: 200
|
|
30
|
+
});
|
|
31
|
+
}, 500);
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const url = `${this.baseUrl}${path}`;
|
|
36
|
+
const defaultHeaders = {
|
|
37
|
+
'Content-Type': 'application/json',
|
|
38
|
+
'Accept': 'application/json'
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
let attempt = 0;
|
|
42
|
+
while (attempt < this.maxRetries) {
|
|
43
|
+
attempt++;
|
|
44
|
+
const controller = new AbortController();
|
|
45
|
+
const id = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
const response = await fetch(url, {
|
|
49
|
+
...options,
|
|
50
|
+
headers: {
|
|
51
|
+
...defaultHeaders,
|
|
52
|
+
...options.headers
|
|
53
|
+
},
|
|
54
|
+
signal: controller.signal
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
clearTimeout(id);
|
|
58
|
+
|
|
59
|
+
// Retry on Server Error (5xx)
|
|
60
|
+
if (response.status >= 500 && attempt < this.maxRetries) {
|
|
61
|
+
console.warn(`[ApiClient] Attempt ${attempt} failed with status ${response.status}. Retrying...`);
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (!response.ok) {
|
|
66
|
+
return {
|
|
67
|
+
data: null,
|
|
68
|
+
error: `HTTP Error: ${response.status} ${response.statusText}`,
|
|
69
|
+
status: response.status
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const data = await response.json();
|
|
74
|
+
return {
|
|
75
|
+
data: data as T,
|
|
76
|
+
error: null,
|
|
77
|
+
status: response.status
|
|
78
|
+
};
|
|
79
|
+
} catch (e: any) {
|
|
80
|
+
clearTimeout(id);
|
|
81
|
+
const isTimeout = e.name === 'AbortError';
|
|
82
|
+
|
|
83
|
+
// Retry on timeout or transient network failures
|
|
84
|
+
if (attempt < this.maxRetries) {
|
|
85
|
+
console.warn(`[ApiClient] Attempt ${attempt} failed: ${e.message}. Retrying...`);
|
|
86
|
+
await new Promise((res) => setTimeout(res, 1000 * attempt)); // Exponential backoff
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
data: null,
|
|
92
|
+
error: isTimeout ? 'Request Timeout' : e.message || 'Unknown network error',
|
|
93
|
+
status: 0
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
data: null,
|
|
100
|
+
error: 'Max retries exceeded',
|
|
101
|
+
status: 0
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export const apiClient = new ApiClient();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "multimodel-dev-os",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"bin": {
|
|
5
5
|
"multimodel-dev-os": "bin/multimodel-dev-os.js"
|
|
6
6
|
},
|
|
@@ -45,7 +45,8 @@
|
|
|
45
45
|
"pack": "bash scripts/pack-template.sh",
|
|
46
46
|
"docs:dev": "vitepress dev docs",
|
|
47
47
|
"docs:build": "vitepress build docs",
|
|
48
|
-
"docs:preview": "vitepress preview docs"
|
|
48
|
+
"docs:preview": "vitepress preview docs",
|
|
49
|
+
"prepublishOnly": "node scripts/prepublish-guard.js"
|
|
49
50
|
},
|
|
50
51
|
"devDependencies": {
|
|
51
52
|
"vitepress": "^1.6.4"
|