@cloudcli-ai/cloudcli 1.29.3 → 1.29.4
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/assets/{index-D-9h5kNf.css → index-BBAE1OJ_.css} +1 -1
- package/dist/assets/{index-y8yqvoLv.js → index-Cyya3LhN.js} +200 -200
- package/dist/index.html +2 -2
- package/dist-server/server/claude-sdk.js +11 -3
- package/dist-server/server/claude-sdk.js.map +1 -1
- package/dist-server/server/cursor-cli.js +7 -1
- package/dist-server/server/cursor-cli.js.map +1 -1
- package/dist-server/server/gemini-cli.js +15 -1
- package/dist-server/server/gemini-cli.js.map +1 -1
- package/dist-server/server/index.js +7 -76
- package/dist-server/server/index.js.map +1 -1
- package/dist-server/server/openai-codex.js +7 -1
- package/dist-server/server/openai-codex.js.map +1 -1
- package/dist-server/server/projects.js +41 -47
- package/dist-server/server/projects.js.map +1 -1
- package/dist-server/server/providers/claude/status.js +121 -0
- package/dist-server/server/providers/claude/status.js.map +1 -0
- package/dist-server/server/providers/codex/status.js +70 -0
- package/dist-server/server/providers/codex/status.js.map +1 -0
- package/dist-server/server/providers/cursor/adapter.js +5 -10
- package/dist-server/server/providers/cursor/adapter.js.map +1 -1
- package/dist-server/server/providers/cursor/status.js +118 -0
- package/dist-server/server/providers/cursor/status.js.map +1 -0
- package/dist-server/server/providers/gemini/status.js +104 -0
- package/dist-server/server/providers/gemini/status.js.map +1 -0
- package/dist-server/server/providers/registry.js +21 -2
- package/dist-server/server/providers/registry.js.map +1 -1
- package/dist-server/server/providers/types.js +11 -0
- package/dist-server/server/providers/types.js.map +1 -1
- package/dist-server/server/routes/cli-auth.js +14 -395
- package/dist-server/server/routes/cli-auth.js.map +1 -1
- package/dist-server/server/routes/cursor.js +6 -22
- package/dist-server/server/routes/cursor.js.map +1 -1
- package/dist-server/server/utils/colors.js +20 -0
- package/dist-server/server/utils/colors.js.map +1 -0
- package/dist-server/server/utils/url-detection.js +58 -0
- package/dist-server/server/utils/url-detection.js.map +1 -0
- package/package.json +3 -5
- package/server/claude-sdk.js +13 -4
- package/server/cursor-cli.js +8 -1
- package/server/gemini-cli.js +17 -1
- package/server/index.js +7 -83
- package/server/openai-codex.js +9 -1
- package/server/projects.js +38 -44
- package/server/providers/claude/status.js +136 -0
- package/server/providers/codex/status.js +78 -0
- package/server/providers/cursor/adapter.js +5 -10
- package/server/providers/cursor/status.js +128 -0
- package/server/providers/gemini/status.js +111 -0
- package/server/providers/registry.js +25 -2
- package/server/providers/types.js +13 -0
- package/server/routes/cli-auth.js +14 -421
- package/server/routes/cursor.js +6 -22
- package/server/utils/colors.js +21 -0
- package/server/utils/url-detection.js +71 -0
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Codex Provider Status
|
|
3
|
+
*
|
|
4
|
+
* Checks whether the user has valid Codex authentication credentials.
|
|
5
|
+
* Codex uses an SDK that makes direct API calls (no external binary),
|
|
6
|
+
* so installation check always returns true if the server is running.
|
|
7
|
+
*
|
|
8
|
+
* @module providers/codex/status
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { promises as fs } from 'fs';
|
|
12
|
+
import path from 'path';
|
|
13
|
+
import os from 'os';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Check if Codex is installed.
|
|
17
|
+
* Codex SDK is bundled with this application — no external binary needed.
|
|
18
|
+
* @returns {boolean}
|
|
19
|
+
*/
|
|
20
|
+
export function checkInstalled() {
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Full status check: installation + authentication.
|
|
26
|
+
* @returns {Promise<import('../types.js').ProviderStatus>}
|
|
27
|
+
*/
|
|
28
|
+
export async function checkStatus() {
|
|
29
|
+
const installed = checkInstalled();
|
|
30
|
+
const result = await checkCredentials();
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
installed,
|
|
34
|
+
authenticated: result.authenticated,
|
|
35
|
+
email: result.email || null,
|
|
36
|
+
error: result.error || null
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ─── Internal helpers ───────────────────────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
async function checkCredentials() {
|
|
43
|
+
try {
|
|
44
|
+
const authPath = path.join(os.homedir(), '.codex', 'auth.json');
|
|
45
|
+
const content = await fs.readFile(authPath, 'utf8');
|
|
46
|
+
const auth = JSON.parse(content);
|
|
47
|
+
|
|
48
|
+
const tokens = auth.tokens || {};
|
|
49
|
+
|
|
50
|
+
if (tokens.id_token || tokens.access_token) {
|
|
51
|
+
let email = 'Authenticated';
|
|
52
|
+
if (tokens.id_token) {
|
|
53
|
+
try {
|
|
54
|
+
const parts = tokens.id_token.split('.');
|
|
55
|
+
if (parts.length >= 2) {
|
|
56
|
+
const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString('utf8'));
|
|
57
|
+
email = payload.email || payload.user || 'Authenticated';
|
|
58
|
+
}
|
|
59
|
+
} catch {
|
|
60
|
+
email = 'Authenticated';
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return { authenticated: true, email };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (auth.OPENAI_API_KEY) {
|
|
68
|
+
return { authenticated: true, email: 'API Key Auth' };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return { authenticated: false, email: null, error: 'No valid tokens found' };
|
|
72
|
+
} catch (error) {
|
|
73
|
+
if (error.code === 'ENOENT') {
|
|
74
|
+
return { authenticated: false, email: null, error: 'Codex not configured' };
|
|
75
|
+
}
|
|
76
|
+
return { authenticated: false, email: null, error: error.message };
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -20,21 +20,16 @@ const PROVIDER = 'cursor';
|
|
|
20
20
|
* @returns {Promise<Array<{id: string, sequence: number, rowid: number, content: object}>>}
|
|
21
21
|
*/
|
|
22
22
|
async function loadCursorBlobs(sessionId, projectPath) {
|
|
23
|
-
// Lazy-import
|
|
24
|
-
const { default:
|
|
25
|
-
const { open } = await import('sqlite');
|
|
23
|
+
// Lazy-import better-sqlite3 so the module doesn't fail if it's unavailable
|
|
24
|
+
const { default: Database } = await import('better-sqlite3');
|
|
26
25
|
|
|
27
26
|
const cwdId = crypto.createHash('md5').update(projectPath || process.cwd()).digest('hex');
|
|
28
27
|
const storeDbPath = path.join(os.homedir(), '.cursor', 'chats', cwdId, sessionId, 'store.db');
|
|
29
28
|
|
|
30
|
-
const db =
|
|
31
|
-
filename: storeDbPath,
|
|
32
|
-
driver: sqlite3.Database,
|
|
33
|
-
mode: sqlite3.OPEN_READONLY,
|
|
34
|
-
});
|
|
29
|
+
const db = new Database(storeDbPath, { readonly: true, fileMustExist: true });
|
|
35
30
|
|
|
36
31
|
try {
|
|
37
|
-
const allBlobs =
|
|
32
|
+
const allBlobs = db.prepare('SELECT rowid, id, data FROM blobs').all();
|
|
38
33
|
|
|
39
34
|
const blobMap = new Map();
|
|
40
35
|
const parentRefs = new Map();
|
|
@@ -129,7 +124,7 @@ async function loadCursorBlobs(sessionId, projectPath) {
|
|
|
129
124
|
|
|
130
125
|
return messages;
|
|
131
126
|
} finally {
|
|
132
|
-
|
|
127
|
+
db.close();
|
|
133
128
|
}
|
|
134
129
|
}
|
|
135
130
|
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cursor Provider Status
|
|
3
|
+
*
|
|
4
|
+
* Checks whether cursor-agent CLI is installed and whether the user
|
|
5
|
+
* is logged in.
|
|
6
|
+
*
|
|
7
|
+
* @module providers/cursor/status
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { execFileSync, spawn } from 'child_process';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Check if cursor-agent CLI is installed.
|
|
14
|
+
* @returns {boolean}
|
|
15
|
+
*/
|
|
16
|
+
export function checkInstalled() {
|
|
17
|
+
try {
|
|
18
|
+
execFileSync('cursor-agent', ['--version'], { stdio: 'ignore', timeout: 5000 });
|
|
19
|
+
return true;
|
|
20
|
+
} catch {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Full status check: installation + authentication.
|
|
27
|
+
* @returns {Promise<import('../types.js').ProviderStatus>}
|
|
28
|
+
*/
|
|
29
|
+
export async function checkStatus() {
|
|
30
|
+
const installed = checkInstalled();
|
|
31
|
+
|
|
32
|
+
if (!installed) {
|
|
33
|
+
return {
|
|
34
|
+
installed,
|
|
35
|
+
authenticated: false,
|
|
36
|
+
email: null,
|
|
37
|
+
error: 'Cursor CLI is not installed'
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const result = await checkCursorLogin();
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
installed,
|
|
45
|
+
authenticated: result.authenticated,
|
|
46
|
+
email: result.email || null,
|
|
47
|
+
error: result.error || null
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ─── Internal helpers ───────────────────────────────────────────────────────
|
|
52
|
+
|
|
53
|
+
function checkCursorLogin() {
|
|
54
|
+
return new Promise((resolve) => {
|
|
55
|
+
let processCompleted = false;
|
|
56
|
+
|
|
57
|
+
const timeout = setTimeout(() => {
|
|
58
|
+
if (!processCompleted) {
|
|
59
|
+
processCompleted = true;
|
|
60
|
+
if (childProcess) {
|
|
61
|
+
childProcess.kill();
|
|
62
|
+
}
|
|
63
|
+
resolve({
|
|
64
|
+
authenticated: false,
|
|
65
|
+
email: null,
|
|
66
|
+
error: 'Command timeout'
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}, 5000);
|
|
70
|
+
|
|
71
|
+
let childProcess;
|
|
72
|
+
try {
|
|
73
|
+
childProcess = spawn('cursor-agent', ['status']);
|
|
74
|
+
} catch {
|
|
75
|
+
clearTimeout(timeout);
|
|
76
|
+
processCompleted = true;
|
|
77
|
+
resolve({
|
|
78
|
+
authenticated: false,
|
|
79
|
+
email: null,
|
|
80
|
+
error: 'Cursor CLI not found or not installed'
|
|
81
|
+
});
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
let stdout = '';
|
|
86
|
+
let stderr = '';
|
|
87
|
+
|
|
88
|
+
childProcess.stdout.on('data', (data) => {
|
|
89
|
+
stdout += data.toString();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
childProcess.stderr.on('data', (data) => {
|
|
93
|
+
stderr += data.toString();
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
childProcess.on('close', (code) => {
|
|
97
|
+
if (processCompleted) return;
|
|
98
|
+
processCompleted = true;
|
|
99
|
+
clearTimeout(timeout);
|
|
100
|
+
|
|
101
|
+
if (code === 0) {
|
|
102
|
+
const emailMatch = stdout.match(/Logged in as ([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})/i);
|
|
103
|
+
|
|
104
|
+
if (emailMatch) {
|
|
105
|
+
resolve({ authenticated: true, email: emailMatch[1] });
|
|
106
|
+
} else if (stdout.includes('Logged in')) {
|
|
107
|
+
resolve({ authenticated: true, email: 'Logged in' });
|
|
108
|
+
} else {
|
|
109
|
+
resolve({ authenticated: false, email: null, error: 'Not logged in' });
|
|
110
|
+
}
|
|
111
|
+
} else {
|
|
112
|
+
resolve({ authenticated: false, email: null, error: stderr || 'Not logged in' });
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
childProcess.on('error', () => {
|
|
117
|
+
if (processCompleted) return;
|
|
118
|
+
processCompleted = true;
|
|
119
|
+
clearTimeout(timeout);
|
|
120
|
+
|
|
121
|
+
resolve({
|
|
122
|
+
authenticated: false,
|
|
123
|
+
email: null,
|
|
124
|
+
error: 'Cursor CLI not found or not installed'
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gemini Provider Status
|
|
3
|
+
*
|
|
4
|
+
* Checks whether Gemini CLI is installed and whether the user
|
|
5
|
+
* has valid authentication credentials.
|
|
6
|
+
*
|
|
7
|
+
* @module providers/gemini/status
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { execFileSync } from 'child_process';
|
|
11
|
+
import { promises as fs } from 'fs';
|
|
12
|
+
import path from 'path';
|
|
13
|
+
import os from 'os';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Check if Gemini CLI is installed.
|
|
17
|
+
* Uses GEMINI_PATH env var if set, otherwise looks for 'gemini' in PATH.
|
|
18
|
+
* @returns {boolean}
|
|
19
|
+
*/
|
|
20
|
+
export function checkInstalled() {
|
|
21
|
+
const cliPath = process.env.GEMINI_PATH || 'gemini';
|
|
22
|
+
try {
|
|
23
|
+
execFileSync(cliPath, ['--version'], { stdio: 'ignore', timeout: 5000 });
|
|
24
|
+
return true;
|
|
25
|
+
} catch {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Full status check: installation + authentication.
|
|
32
|
+
* @returns {Promise<import('../types.js').ProviderStatus>}
|
|
33
|
+
*/
|
|
34
|
+
export async function checkStatus() {
|
|
35
|
+
const installed = checkInstalled();
|
|
36
|
+
|
|
37
|
+
if (!installed) {
|
|
38
|
+
return {
|
|
39
|
+
installed,
|
|
40
|
+
authenticated: false,
|
|
41
|
+
email: null,
|
|
42
|
+
error: 'Gemini CLI is not installed'
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const result = await checkCredentials();
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
installed,
|
|
50
|
+
authenticated: result.authenticated,
|
|
51
|
+
email: result.email || null,
|
|
52
|
+
error: result.error || null
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ─── Internal helpers ───────────────────────────────────────────────────────
|
|
57
|
+
|
|
58
|
+
async function checkCredentials() {
|
|
59
|
+
if (process.env.GEMINI_API_KEY && process.env.GEMINI_API_KEY.trim()) {
|
|
60
|
+
return { authenticated: true, email: 'API Key Auth' };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
const credsPath = path.join(os.homedir(), '.gemini', 'oauth_creds.json');
|
|
65
|
+
const content = await fs.readFile(credsPath, 'utf8');
|
|
66
|
+
const creds = JSON.parse(content);
|
|
67
|
+
|
|
68
|
+
if (creds.access_token) {
|
|
69
|
+
let email = 'OAuth Session';
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
const tokenRes = await fetch(`https://oauth2.googleapis.com/tokeninfo?access_token=${creds.access_token}`);
|
|
73
|
+
if (tokenRes.ok) {
|
|
74
|
+
const tokenInfo = await tokenRes.json();
|
|
75
|
+
if (tokenInfo.email) {
|
|
76
|
+
email = tokenInfo.email;
|
|
77
|
+
}
|
|
78
|
+
} else if (!creds.refresh_token) {
|
|
79
|
+
return {
|
|
80
|
+
authenticated: false,
|
|
81
|
+
email: null,
|
|
82
|
+
error: 'Access token invalid and no refresh token found'
|
|
83
|
+
};
|
|
84
|
+
} else {
|
|
85
|
+
// Token might be expired but we have a refresh token, so CLI will refresh it
|
|
86
|
+
email = await getActiveAccountEmail() || email;
|
|
87
|
+
}
|
|
88
|
+
} catch {
|
|
89
|
+
// Network error, fallback to checking local accounts file
|
|
90
|
+
email = await getActiveAccountEmail() || email;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return { authenticated: true, email };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return { authenticated: false, email: null, error: 'No valid tokens found in oauth_creds' };
|
|
97
|
+
} catch {
|
|
98
|
+
return { authenticated: false, email: null, error: 'Gemini CLI not configured' };
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function getActiveAccountEmail() {
|
|
103
|
+
try {
|
|
104
|
+
const accPath = path.join(os.homedir(), '.gemini', 'google_accounts.json');
|
|
105
|
+
const accContent = await fs.readFile(accPath, 'utf8');
|
|
106
|
+
const accounts = JSON.parse(accContent);
|
|
107
|
+
return accounts.active || null;
|
|
108
|
+
} catch {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Provider Registry
|
|
3
3
|
*
|
|
4
|
-
* Centralizes provider adapter lookup. All code that needs
|
|
5
|
-
* should go through this registry instead
|
|
4
|
+
* Centralizes provider adapter and status checker lookup. All code that needs
|
|
5
|
+
* a provider adapter or status checker should go through this registry instead
|
|
6
|
+
* of importing individual modules directly.
|
|
6
7
|
*
|
|
7
8
|
* @module providers/registry
|
|
8
9
|
*/
|
|
@@ -12,6 +13,11 @@ import { cursorAdapter } from './cursor/adapter.js';
|
|
|
12
13
|
import { codexAdapter } from './codex/adapter.js';
|
|
13
14
|
import { geminiAdapter } from './gemini/adapter.js';
|
|
14
15
|
|
|
16
|
+
import * as claudeStatus from './claude/status.js';
|
|
17
|
+
import * as cursorStatus from './cursor/status.js';
|
|
18
|
+
import * as codexStatus from './codex/status.js';
|
|
19
|
+
import * as geminiStatus from './gemini/status.js';
|
|
20
|
+
|
|
15
21
|
/**
|
|
16
22
|
* @typedef {import('./types.js').ProviderAdapter} ProviderAdapter
|
|
17
23
|
* @typedef {import('./types.js').SessionProvider} SessionProvider
|
|
@@ -20,12 +26,20 @@ import { geminiAdapter } from './gemini/adapter.js';
|
|
|
20
26
|
/** @type {Map<string, ProviderAdapter>} */
|
|
21
27
|
const providers = new Map();
|
|
22
28
|
|
|
29
|
+
/** @type {Map<string, { checkInstalled: () => boolean, checkStatus: () => Promise<import('./types.js').ProviderStatus> }>} */
|
|
30
|
+
const statusCheckers = new Map();
|
|
31
|
+
|
|
23
32
|
// Register built-in providers
|
|
24
33
|
providers.set('claude', claudeAdapter);
|
|
25
34
|
providers.set('cursor', cursorAdapter);
|
|
26
35
|
providers.set('codex', codexAdapter);
|
|
27
36
|
providers.set('gemini', geminiAdapter);
|
|
28
37
|
|
|
38
|
+
statusCheckers.set('claude', claudeStatus);
|
|
39
|
+
statusCheckers.set('cursor', cursorStatus);
|
|
40
|
+
statusCheckers.set('codex', codexStatus);
|
|
41
|
+
statusCheckers.set('gemini', geminiStatus);
|
|
42
|
+
|
|
29
43
|
/**
|
|
30
44
|
* Get a provider adapter by name.
|
|
31
45
|
* @param {string} name - Provider name (e.g., 'claude', 'cursor', 'codex', 'gemini')
|
|
@@ -35,6 +49,15 @@ export function getProvider(name) {
|
|
|
35
49
|
return providers.get(name);
|
|
36
50
|
}
|
|
37
51
|
|
|
52
|
+
/**
|
|
53
|
+
* Get a provider status checker by name.
|
|
54
|
+
* @param {string} name - Provider name
|
|
55
|
+
* @returns {{ checkInstalled: () => boolean, checkStatus: () => Promise<import('./types.js').ProviderStatus> } | undefined}
|
|
56
|
+
*/
|
|
57
|
+
export function getStatusChecker(name) {
|
|
58
|
+
return statusCheckers.get(name);
|
|
59
|
+
}
|
|
60
|
+
|
|
38
61
|
/**
|
|
39
62
|
* Get all registered provider names.
|
|
40
63
|
* @returns {string[]}
|
|
@@ -69,6 +69,19 @@
|
|
|
69
69
|
* @property {object} [tokenUsage] - Token usage data (provider-specific)
|
|
70
70
|
*/
|
|
71
71
|
|
|
72
|
+
// ─── Provider Status ────────────────────────────────────────────────────────
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Result of a provider status check (installation + authentication).
|
|
76
|
+
*
|
|
77
|
+
* @typedef {Object} ProviderStatus
|
|
78
|
+
* @property {boolean} installed - Whether the provider's CLI/SDK is available
|
|
79
|
+
* @property {boolean} authenticated - Whether valid credentials exist
|
|
80
|
+
* @property {string|null} email - User email or auth method identifier
|
|
81
|
+
* @property {string|null} [method] - Auth method (e.g. 'api_key', 'credentials_file')
|
|
82
|
+
* @property {string|null} [error] - Error message if not installed or not authenticated
|
|
83
|
+
*/
|
|
84
|
+
|
|
72
85
|
// ─── Provider Adapter Interface ──────────────────────────────────────────────
|
|
73
86
|
|
|
74
87
|
/**
|