prism-mcp-server 12.5.2 → 12.5.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/auth.js DELETED
@@ -1,218 +0,0 @@
1
- /**
2
- * Prism Cloud — Authentication Module (v2.0)
3
- * =============================================
4
- *
5
- * Handles OAuth login flow for Prism CLI:
6
- * 1. `prism login` → starts local HTTP server on random port
7
- * 2. Opens browser → Synalux OAuth page with redirect back to localhost
8
- * 3. Receives auth code → exchanges for JWT + refresh token
9
- * 4. Stores tokens in prism-config.db via configStorage
10
- *
11
- * Token management:
12
- * - Auto-refresh expired JWTs using stored refresh token
13
- * - `prism logout` → clears stored tokens
14
- * - `prism status` → shows current auth state
15
- */
16
- import { createServer } from 'http';
17
- import { URL } from 'url';
18
- import { setSetting, getSetting } from './storage/configStorage.js';
19
- import { clearCloudCache } from './prism-cloud.js';
20
- // ─── Config ───────────────────────────────────────────────────────
21
- const SYNALUX_BASE = process.env.SYNALUX_API_BASE || 'https://synalux.ai';
22
- const AUTH_CALLBACK_PATH = '/auth/callback';
23
- // Config keys in prism-config.db
24
- const KEY_AUTH_TOKEN = 'prism_auth_token';
25
- const KEY_REFRESH_TOKEN = 'prism_refresh_token';
26
- const KEY_AUTH_EMAIL = 'prism_auth_email';
27
- const KEY_AUTH_NAME = 'prism_auth_name';
28
- const KEY_AUTH_PLAN = 'prism_auth_plan';
29
- const KEY_AUTH_EXPIRES = 'prism_auth_expires';
30
- /**
31
- * Start the OAuth login flow.
32
- * Opens default browser to Synalux login page, waits for callback.
33
- */
34
- export async function login() {
35
- return new Promise((resolve) => {
36
- // Start local server on random port
37
- const server = createServer(async (req, res) => {
38
- const url = new URL(req.url || '/', `http://localhost`);
39
- if (url.pathname !== AUTH_CALLBACK_PATH) {
40
- res.writeHead(404);
41
- res.end('Not found');
42
- return;
43
- }
44
- const code = url.searchParams.get('code');
45
- const error = url.searchParams.get('error');
46
- if (error) {
47
- res.writeHead(200, { 'Content-Type': 'text/html' });
48
- res.end(renderHtml('Login Failed', `Error: ${error}. You can close this window.`));
49
- server.close();
50
- resolve({ success: false, error });
51
- return;
52
- }
53
- if (!code) {
54
- res.writeHead(400, { 'Content-Type': 'text/html' });
55
- res.end(renderHtml('Login Failed', 'No authorization code received. You can close this window.'));
56
- server.close();
57
- resolve({ success: false, error: 'No authorization code' });
58
- return;
59
- }
60
- try {
61
- // Exchange code for JWT
62
- const tokenResult = await exchangeCode(code);
63
- // Store tokens
64
- await setSetting(KEY_AUTH_TOKEN, tokenResult.access_token);
65
- if (tokenResult.refresh_token) {
66
- await setSetting(KEY_REFRESH_TOKEN, tokenResult.refresh_token);
67
- }
68
- if (tokenResult.email) {
69
- await setSetting(KEY_AUTH_EMAIL, tokenResult.email);
70
- }
71
- if (tokenResult.plan) {
72
- await setSetting(KEY_AUTH_PLAN, tokenResult.plan);
73
- }
74
- if (tokenResult.expires_at) {
75
- await setSetting(KEY_AUTH_EXPIRES, String(tokenResult.expires_at));
76
- }
77
- // Clear cached cloud limits so next verify call uses new token
78
- clearCloudCache();
79
- res.writeHead(200, { 'Content-Type': 'text/html' });
80
- res.end(renderHtml('✅ Prism Login Successful', `Authenticated as <strong>${tokenResult.email}</strong> (${tokenResult.plan} plan). You can close this window.`));
81
- server.close();
82
- resolve({
83
- success: true,
84
- email: tokenResult.email,
85
- plan: tokenResult.plan,
86
- });
87
- }
88
- catch (err) {
89
- const message = err instanceof Error ? err.message : String(err);
90
- res.writeHead(200, { 'Content-Type': 'text/html' });
91
- res.end(renderHtml('Login Failed', `Error: ${message}. You can close this window.`));
92
- server.close();
93
- resolve({ success: false, error: message });
94
- }
95
- });
96
- server.listen(0, '127.0.0.1', () => {
97
- const addr = server.address();
98
- if (!addr || typeof addr === 'string') {
99
- resolve({ success: false, error: 'Failed to start local server' });
100
- return;
101
- }
102
- const port = addr.port;
103
- const callbackUrl = `http://127.0.0.1:${port}${AUTH_CALLBACK_PATH}`;
104
- const loginUrl = `${SYNALUX_BASE}/auth/prism-login?redirect_uri=${encodeURIComponent(callbackUrl)}`;
105
- console.log(`\n🔐 Opening browser for Synalux login...`);
106
- console.log(` If the browser doesn't open, visit:\n ${loginUrl}\n`);
107
- // Open browser (platform-agnostic)
108
- openBrowser(loginUrl);
109
- // Timeout after 5 minutes
110
- setTimeout(() => {
111
- server.close();
112
- resolve({ success: false, error: 'Login timed out (5 minutes)' });
113
- }, 5 * 60 * 1000);
114
- });
115
- });
116
- }
117
- async function exchangeCode(code) {
118
- const response = await fetch(`${SYNALUX_BASE}/api/v1/auth/code-exchange`, {
119
- method: 'POST',
120
- headers: { 'Content-Type': 'application/json' },
121
- body: JSON.stringify({ code, client: 'prism-cli' }),
122
- });
123
- if (!response.ok) {
124
- const text = await response.text();
125
- throw new Error(`Token exchange failed (${response.status}): ${text}`);
126
- }
127
- return response.json();
128
- }
129
- // ─── Token Refresh ────────────────────────────────────────────────
130
- /**
131
- * Get a valid auth token, refreshing if expired.
132
- * Returns empty string if not logged in.
133
- */
134
- export async function getAuthToken() {
135
- const token = await getSetting(KEY_AUTH_TOKEN);
136
- if (!token)
137
- return '';
138
- // Check expiry
139
- const expiresStr = await getSetting(KEY_AUTH_EXPIRES);
140
- if (expiresStr) {
141
- const expiresAt = parseInt(expiresStr, 10);
142
- const now = Math.floor(Date.now() / 1000);
143
- if (now < expiresAt - 60) {
144
- // Token still valid (with 60s buffer)
145
- return token;
146
- }
147
- // Try to refresh
148
- const refreshToken = await getSetting(KEY_REFRESH_TOKEN);
149
- if (refreshToken) {
150
- try {
151
- const refreshed = await refreshAuthToken(refreshToken);
152
- await setSetting(KEY_AUTH_TOKEN, refreshed.access_token);
153
- if (refreshed.expires_at) {
154
- await setSetting(KEY_AUTH_EXPIRES, String(refreshed.expires_at));
155
- }
156
- clearCloudCache();
157
- return refreshed.access_token;
158
- }
159
- catch {
160
- console.warn('[Prism Auth] Token refresh failed. Run `prism login` to re-authenticate.');
161
- return '';
162
- }
163
- }
164
- }
165
- return token;
166
- }
167
- async function refreshAuthToken(refreshToken) {
168
- const response = await fetch(`${SYNALUX_BASE}/api/v1/auth/jwt`, {
169
- method: 'POST',
170
- headers: { 'Content-Type': 'application/json' },
171
- body: JSON.stringify({ refresh_token: refreshToken, client: 'prism-cli' }),
172
- });
173
- if (!response.ok) {
174
- throw new Error(`Token refresh failed (${response.status})`);
175
- }
176
- return response.json();
177
- }
178
- // ─── Logout ───────────────────────────────────────────────────────
179
- export async function logout() {
180
- await setSetting(KEY_AUTH_TOKEN, '');
181
- await setSetting(KEY_REFRESH_TOKEN, '');
182
- await setSetting(KEY_AUTH_EMAIL, '');
183
- await setSetting(KEY_AUTH_PLAN, '');
184
- await setSetting(KEY_AUTH_EXPIRES, '');
185
- clearCloudCache();
186
- }
187
- export async function getAuthStatus() {
188
- const token = await getSetting(KEY_AUTH_TOKEN);
189
- if (!token)
190
- return { loggedIn: false };
191
- const email = await getSetting(KEY_AUTH_EMAIL);
192
- const name = await getSetting(KEY_AUTH_NAME);
193
- const plan = await getSetting(KEY_AUTH_PLAN);
194
- const expiresStr = await getSetting(KEY_AUTH_EXPIRES);
195
- return {
196
- loggedIn: true,
197
- email: email || undefined,
198
- name: name || undefined,
199
- plan: plan || undefined,
200
- expiresAt: expiresStr ? new Date(parseInt(expiresStr, 10) * 1000) : undefined,
201
- };
202
- }
203
- // ─── Helpers ──────────────────────────────────────────────────────
204
- function renderHtml(title, body) {
205
- return `<!DOCTYPE html>
206
- <html><head><title>${title}</title>
207
- <style>body{font-family:system-ui,-apple-system,sans-serif;display:flex;justify-content:center;align-items:center;min-height:100vh;margin:0;background:#0a0a1a;color:#e0e0e0}
208
- .card{background:#1a1a2e;border-radius:16px;padding:48px;max-width:480px;text-align:center;box-shadow:0 8px 32px rgba(0,0,0,.4)}
209
- h1{font-size:24px;margin-bottom:16px}p{color:#a0a0b0;line-height:1.6}</style>
210
- </head><body><div class="card"><h1>${title}</h1><p>${body}</p></div></body></html>`;
211
- }
212
- function openBrowser(url) {
213
- const { exec } = require('child_process');
214
- const cmd = process.platform === 'darwin' ? 'open'
215
- : process.platform === 'win32' ? 'start'
216
- : 'xdg-open';
217
- exec(`${cmd} "${url}"`);
218
- }
@@ -1,110 +0,0 @@
1
- import { getSettingSync } from './storage/configStorage.js';
2
- const DEFAULT_LIMITS = {
3
- tier: 'free',
4
- status: 'active',
5
- limits: {
6
- llm_daily: 0,
7
- search_daily: 0,
8
- memory_sync: false,
9
- coder_daily: 0,
10
- },
11
- usage: {
12
- llm_calls: 0,
13
- search_calls: 0,
14
- memory_calls: 0,
15
- coder_calls: 0,
16
- },
17
- features: {
18
- cloud_coder: false,
19
- cloud_memory: false,
20
- cloud_llm: false,
21
- cloud_search: false,
22
- hivemind: false,
23
- voice_video: false,
24
- },
25
- };
26
- let cachedLimits = null;
27
- /**
28
- * Verify cloud license via Synalux API (v2.0 — JWT Auth).
29
- *
30
- * Reads auth token from:
31
- * 1. PRISM_AUTH_TOKEN env var (set by `prism login`)
32
- * 2. prism_auth_token setting (stored by dashboard login)
33
- * 3. Legacy: PRISM_LICENSE_KEY (deprecated, returns guidance)
34
- *
35
- * Calls GET /api/v1/prism/verify with Bearer JWT.
36
- */
37
- export async function verifyCloudLicense() {
38
- if (cachedLimits)
39
- return cachedLimits;
40
- // v2.0: Read JWT auth token (set by `prism login` OAuth flow)
41
- const authToken = getSettingSync('prism_auth_token', process.env.PRISM_AUTH_TOKEN || '');
42
- // Legacy fallback: if user still has old license key, guide them to upgrade
43
- const legacyKey = getSettingSync('prism_license_key', process.env.PRISM_LICENSE_KEY || '');
44
- if (!authToken && legacyKey) {
45
- console.warn('[Prism Cloud] License key auth is deprecated. Please run `prism login` to switch to OAuth.');
46
- return DEFAULT_LIMITS;
47
- }
48
- if (!authToken) {
49
- console.warn('[Prism Cloud] No auth token found. Operating in Free tier. Run `prism login` to authenticate.');
50
- return DEFAULT_LIMITS;
51
- }
52
- try {
53
- const baseUrl = process.env.SYNALUX_API_BASE || 'https://synalux.ai';
54
- const response = await fetch(`${baseUrl}/api/v1/prism/verify`, {
55
- method: 'GET',
56
- headers: {
57
- 'Authorization': `Bearer ${authToken}`,
58
- 'Content-Type': 'application/json',
59
- },
60
- });
61
- if (!response.ok) {
62
- if (response.status === 401) {
63
- console.warn('[Prism Cloud] Auth token expired or invalid. Run `prism login` to re-authenticate.');
64
- }
65
- else {
66
- console.warn(`[Prism Cloud] Verification failed with status ${response.status}. Operating in Free tier.`);
67
- }
68
- return DEFAULT_LIMITS;
69
- }
70
- const data = await response.json();
71
- if (data.status === 'success') {
72
- cachedLimits = {
73
- tier: data.user.plan || 'free',
74
- status: 'active',
75
- limits: {
76
- llm_daily: data.limits?.llm_daily || 0,
77
- search_daily: data.limits?.search_daily || 0,
78
- memory_sync: data.limits?.memory_sync || false,
79
- coder_daily: data.limits?.coder_daily || 0,
80
- },
81
- usage: {
82
- llm_calls: data.usage?.llm_calls || 0,
83
- search_calls: data.usage?.search_calls || 0,
84
- memory_calls: data.usage?.memory_calls || 0,
85
- coder_calls: data.usage?.coder_calls || 0,
86
- },
87
- features: data.features || DEFAULT_LIMITS.features,
88
- };
89
- console.log(`[Prism Cloud] Verified ${cachedLimits.tier} license.`);
90
- return cachedLimits;
91
- }
92
- else {
93
- console.warn(`[Prism Cloud] Invalid response. Operating in Free tier.`);
94
- return DEFAULT_LIMITS;
95
- }
96
- }
97
- catch (error) {
98
- console.error(`[Prism Cloud] Error verifying license: ${error instanceof Error ? error.message : String(error)}`);
99
- return DEFAULT_LIMITS;
100
- }
101
- }
102
- export function getCloudLimits() {
103
- return cachedLimits || DEFAULT_LIMITS;
104
- }
105
- /**
106
- * Clear cached limits (e.g., after `prism login` sets new token).
107
- */
108
- export function clearCloudCache() {
109
- cachedLimits = null;
110
- }
@@ -1,214 +0,0 @@
1
- /**
2
- * Prism Projects — MCP Tool Handlers (Phase 4)
3
- * ===============================================
4
- *
5
- * CRUD operations for projects and team membership.
6
- * Tier limits enforced via getCloudLimits().
7
- *
8
- * Tools:
9
- * - project_create: Create a new project
10
- * - project_list: List all projects for current user
11
- * - project_update: Update project name/description/status
12
- * - project_delete: Archive/delete a project
13
- * - project_assign_member: Add a member to a project
14
- * - project_remove_member: Remove a member from a project
15
- */
16
- import { randomUUID } from 'crypto';
17
- import { getStorage } from '../storage/index.js';
18
- import { getCloudLimits } from '../prism-cloud.js';
19
- import { PRISM_USER_ID } from '../config.js';
20
- const VALID_STATUSES = ['draft', 'active', 'on_hold', 'completed', 'archived'];
21
- const VALID_ROLES = ['owner', 'editor', 'viewer'];
22
- // Tier limits for projects
23
- const PROJECT_LIMITS = {
24
- free: { maxProjects: 1, maxMembers: 1 },
25
- standard: { maxProjects: 9, maxMembers: 5 },
26
- advanced: { maxProjects: 999, maxMembers: 25 },
27
- enterprise: { maxProjects: 999999, maxMembers: 999999 },
28
- };
29
- function makeResult(text, isError = false) {
30
- return { content: [{ type: 'text', text }], isError };
31
- }
32
- // ─── project_create ──────────────────────────────────────────
33
- export async function projectCreateHandler(args) {
34
- const { name, description, status, team_id } = args;
35
- if (!name || typeof name !== 'string' || name.trim().length === 0) {
36
- return makeResult('❌ Missing required field: name', true);
37
- }
38
- if (status && !VALID_STATUSES.includes(status)) {
39
- return makeResult(`❌ Invalid status. Must be one of: ${VALID_STATUSES.join(', ')}`, true);
40
- }
41
- // Check tier limits
42
- const limits = getCloudLimits();
43
- const tierLimits = PROJECT_LIMITS[limits.tier] || PROJECT_LIMITS.free;
44
- const storage = await getStorage();
45
- const db = storage.db;
46
- if (!db) {
47
- return makeResult('❌ Project management requires SQLite storage.', true);
48
- }
49
- // Count existing projects
50
- const countResult = await db.execute({
51
- sql: 'SELECT COUNT(*) as cnt FROM prism_projects WHERE created_by = ?',
52
- args: [PRISM_USER_ID],
53
- });
54
- const currentCount = Number(countResult.rows[0]?.cnt || 0);
55
- if (currentCount >= tierLimits.maxProjects) {
56
- return makeResult(`❌ Project limit reached (${currentCount}/${tierLimits.maxProjects}). ` +
57
- `Upgrade your plan to create more projects.`, true);
58
- }
59
- const id = randomUUID();
60
- await db.execute({
61
- sql: `INSERT INTO prism_projects (id, name, description, status, created_by, team_id)
62
- VALUES (?, ?, ?, ?, ?, ?)`,
63
- args: [id, name.trim(), description || '', status || 'draft', PRISM_USER_ID, team_id || null],
64
- });
65
- // Auto-assign creator as owner
66
- await db.execute({
67
- sql: `INSERT INTO prism_project_members (project_id, user_id, role)
68
- VALUES (?, ?, 'owner')`,
69
- args: [id, PRISM_USER_ID],
70
- });
71
- return makeResult(`✅ Project created: "${name.trim()}" (${id})\n` +
72
- ` Status: ${status || 'draft'}\n` +
73
- ` Projects used: ${currentCount + 1}/${tierLimits.maxProjects}`);
74
- }
75
- // ─── project_list ────────────────────────────────────────────
76
- export async function projectListHandler(args) {
77
- const storage = await getStorage();
78
- const db = storage.db;
79
- if (!db) {
80
- return makeResult('❌ Project management requires SQLite storage.', true);
81
- }
82
- let sql = `SELECT p.*, m.role as my_role
83
- FROM prism_projects p
84
- LEFT JOIN prism_project_members m ON p.id = m.project_id AND m.user_id = ?
85
- WHERE (p.created_by = ? OR m.user_id = ?)`;
86
- const sqlArgs = [PRISM_USER_ID, PRISM_USER_ID, PRISM_USER_ID];
87
- if (args.status) {
88
- sql += ` AND p.status = ?`;
89
- sqlArgs.push(args.status);
90
- }
91
- if (args.team_id) {
92
- sql += ` AND p.team_id = ?`;
93
- sqlArgs.push(args.team_id);
94
- }
95
- sql += ` ORDER BY p.updated_at DESC`;
96
- const result = await db.execute({ sql, args: sqlArgs });
97
- const projects = result.rows;
98
- if (projects.length === 0) {
99
- return makeResult('📁 No projects found. Use `project_create` to create one.');
100
- }
101
- const lines = projects.map((p) => {
102
- const statusIcon = p.status === 'active' ? '🟢' : p.status === 'draft' ? '📝' : p.status === 'completed' ? '✅' : p.status === 'archived' ? '📦' : '⏸️';
103
- return `${statusIcon} **${p.name}** (${p.id.substring(0, 8)})\n Status: ${p.status} | Role: ${p.my_role || 'owner'} | Updated: ${p.updated_at}`;
104
- });
105
- const limits = getCloudLimits();
106
- const tierLimits = PROJECT_LIMITS[limits.tier] || PROJECT_LIMITS.free;
107
- return makeResult(`📁 **Projects** (${projects.length}/${tierLimits.maxProjects})\n\n` +
108
- lines.join('\n\n'));
109
- }
110
- // ─── project_update ──────────────────────────────────────────
111
- export async function projectUpdateHandler(args) {
112
- const { project_id, name, description, status } = args;
113
- if (!project_id) {
114
- return makeResult('❌ Missing required field: project_id', true);
115
- }
116
- if (status && !VALID_STATUSES.includes(status)) {
117
- return makeResult(`❌ Invalid status. Must be one of: ${VALID_STATUSES.join(', ')}`, true);
118
- }
119
- const storage = await getStorage();
120
- const db = storage.db;
121
- if (!db)
122
- return makeResult('❌ Project management requires SQLite storage.', true);
123
- const sets = ["updated_at = datetime('now')"];
124
- const sqlArgs = [];
125
- if (name) {
126
- sets.push('name = ?');
127
- sqlArgs.push(name.trim());
128
- }
129
- if (description !== undefined) {
130
- sets.push('description = ?');
131
- sqlArgs.push(description);
132
- }
133
- if (status) {
134
- sets.push('status = ?');
135
- sqlArgs.push(status);
136
- }
137
- sqlArgs.push(project_id);
138
- await db.execute({
139
- sql: `UPDATE prism_projects SET ${sets.join(', ')} WHERE id = ?`,
140
- args: sqlArgs,
141
- });
142
- return makeResult(`✅ Project ${project_id.substring(0, 8)} updated.`);
143
- }
144
- // ─── project_delete ──────────────────────────────────────────
145
- export async function projectDeleteHandler(args) {
146
- const { project_id, hard } = args;
147
- if (!project_id) {
148
- return makeResult('❌ Missing required field: project_id', true);
149
- }
150
- const storage = await getStorage();
151
- const db = storage.db;
152
- if (!db)
153
- return makeResult('❌ Project management requires SQLite storage.', true);
154
- if (hard) {
155
- await db.execute({ sql: 'DELETE FROM prism_projects WHERE id = ?', args: [project_id] });
156
- return makeResult(`🗑️ Project ${project_id.substring(0, 8)} permanently deleted.`);
157
- }
158
- else {
159
- await db.execute({
160
- sql: `UPDATE prism_projects SET status = 'archived', updated_at = datetime('now') WHERE id = ?`,
161
- args: [project_id],
162
- });
163
- return makeResult(`📦 Project ${project_id.substring(0, 8)} archived.`);
164
- }
165
- }
166
- // ─── project_assign_member ───────────────────────────────────
167
- export async function projectAssignMemberHandler(args) {
168
- const { project_id, user_id, role } = args;
169
- if (!project_id || !user_id) {
170
- return makeResult('❌ Missing required fields: project_id, user_id', true);
171
- }
172
- const effectiveRole = role || 'viewer';
173
- if (!VALID_ROLES.includes(effectiveRole)) {
174
- return makeResult(`❌ Invalid role. Must be one of: ${VALID_ROLES.join(', ')}`, true);
175
- }
176
- // Check tier limits
177
- const limits = getCloudLimits();
178
- const tierLimits = PROJECT_LIMITS[limits.tier] || PROJECT_LIMITS.free;
179
- const storage = await getStorage();
180
- const db = storage.db;
181
- if (!db)
182
- return makeResult('❌ Project management requires SQLite storage.', true);
183
- const countResult = await db.execute({
184
- sql: 'SELECT COUNT(*) as cnt FROM prism_project_members WHERE project_id = ?',
185
- args: [project_id],
186
- });
187
- const currentMembers = Number(countResult.rows[0]?.cnt || 0);
188
- if (currentMembers >= tierLimits.maxMembers) {
189
- return makeResult(`❌ Member limit reached (${currentMembers}/${tierLimits.maxMembers}). ` +
190
- `Upgrade your plan to add more members.`, true);
191
- }
192
- await db.execute({
193
- sql: `INSERT OR REPLACE INTO prism_project_members (project_id, user_id, role)
194
- VALUES (?, ?, ?)`,
195
- args: [project_id, user_id, effectiveRole],
196
- });
197
- return makeResult(`✅ ${user_id} assigned as ${effectiveRole} to project ${project_id.substring(0, 8)}.`);
198
- }
199
- // ─── project_remove_member ───────────────────────────────────
200
- export async function projectRemoveMemberHandler(args) {
201
- const { project_id, user_id } = args;
202
- if (!project_id || !user_id) {
203
- return makeResult('❌ Missing required fields: project_id, user_id', true);
204
- }
205
- const storage = await getStorage();
206
- const db = storage.db;
207
- if (!db)
208
- return makeResult('❌ Project management requires SQLite storage.', true);
209
- await db.execute({
210
- sql: 'DELETE FROM prism_project_members WHERE project_id = ? AND user_id = ?',
211
- args: [project_id, user_id],
212
- });
213
- return makeResult(`✅ ${user_id} removed from project ${project_id.substring(0, 8)}.`);
214
- }