fraim 2.0.202 → 2.0.204

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.
Files changed (79) hide show
  1. package/README.md +3 -3
  2. package/bin/fraim.js +8 -18
  3. package/dist/src/cli/commands/first-run.js +2 -37
  4. package/dist/src/cli/doctor/checks/mcp-connectivity-checks.js +1 -1
  5. package/dist/src/cli/fraim.js +0 -2
  6. package/dist/src/cli/setup/ide-invocation-surfaces.js +6 -6
  7. package/dist/src/cli/utils/managed-agent-paths.js +76 -6
  8. package/dist/src/cli/utils/version-utils.js +2 -2
  9. package/dist/src/config/persona-capability-bundles.js +3 -3
  10. package/dist/src/core/job-phases.js +168 -0
  11. package/dist/src/core/utils/ports.js +32 -0
  12. package/dist/src/core/utils/stub-generator.js +24 -2
  13. package/dist/src/first-run/session-service.js +106 -24
  14. package/dist/src/local-mcp-server/learning-context-builder.js +1 -1
  15. package/dist/src/local-mcp-server/stdio-server.js +41 -9
  16. package/dist/src/routes/analytics.js +2 -2
  17. package/dist/src/services/registry-service.js +2 -2
  18. package/index.js +1 -1
  19. package/package.json +61 -181
  20. package/dist/src/ai-hub/catalog.js +0 -406
  21. package/dist/src/ai-hub/cert-store.js +0 -70
  22. package/dist/src/ai-hub/conversation-store.js +0 -359
  23. package/dist/src/ai-hub/desktop-main.js +0 -449
  24. package/dist/src/ai-hub/hosts.js +0 -1664
  25. package/dist/src/ai-hub/hub-latest-version.js +0 -52
  26. package/dist/src/ai-hub/hub-launch-decision.js +0 -57
  27. package/dist/src/ai-hub/hub-runtime-file.js +0 -43
  28. package/dist/src/ai-hub/managed-browser.js +0 -269
  29. package/dist/src/ai-hub/manager-turns.js +0 -64
  30. package/dist/src/ai-hub/office-sideload.js +0 -156
  31. package/dist/src/ai-hub/openclaw-bridge.js +0 -250
  32. package/dist/src/ai-hub/preferences.js +0 -201
  33. package/dist/src/ai-hub/remote-hub-gateway.js +0 -88
  34. package/dist/src/ai-hub/server.js +0 -3827
  35. package/dist/src/ai-hub/types.js +0 -2
  36. package/dist/src/ai-hub/word-sideload.js +0 -95
  37. package/dist/src/cli/commands/hub.js +0 -238
  38. package/dist/src/cli/commands/test-mcp.js +0 -171
  39. package/dist/src/cli/setup/first-run.js +0 -242
  40. package/dist/src/core/config-writer.js +0 -75
  41. package/dist/src/core/utils/job-aliases.js +0 -47
  42. package/dist/src/core/utils/workflow-parser.js +0 -174
  43. package/dist/src/services/email-service-clean.js +0 -782
  44. package/extensions/office-word/favicon.ico +0 -0
  45. package/extensions/office-word/icon-64.png +0 -0
  46. package/extensions/office-word/manifest.xml +0 -33
  47. package/extensions/office-word/taskpane.html +0 -242
  48. package/public/ai-hub/index.html +0 -894
  49. package/public/ai-hub/powerpoint-taskpane/icon-64.png +0 -0
  50. package/public/ai-hub/powerpoint-taskpane/index.html +0 -236
  51. package/public/ai-hub/powerpoint-taskpane/manifest.xml +0 -30
  52. package/public/ai-hub/review.css +0 -381
  53. package/public/ai-hub/script.js +0 -11024
  54. package/public/ai-hub/styles.css +0 -4873
  55. package/public/portfolio/ashley.html +0 -523
  56. package/public/portfolio/auditya.html +0 -83
  57. package/public/portfolio/banke.html +0 -83
  58. package/public/portfolio/beza.html +0 -659
  59. package/public/portfolio/careena.html +0 -632
  60. package/public/portfolio/casey.html +0 -568
  61. package/public/portfolio/celia.html +0 -490
  62. package/public/portfolio/deidre.html +0 -642
  63. package/public/portfolio/gautam.html +0 -597
  64. package/public/portfolio/hari.html +0 -469
  65. package/public/portfolio/huxley.html +0 -1354
  66. package/public/portfolio/index.html +0 -741
  67. package/public/portfolio/maestro.html +0 -518
  68. package/public/portfolio/mandy.html +0 -590
  69. package/public/portfolio/mona.html +0 -597
  70. package/public/portfolio/pam.html +0 -887
  71. package/public/portfolio/procella.html +0 -107
  72. package/public/portfolio/qasm.html +0 -569
  73. package/public/portfolio/ricardo.html +0 -489
  74. package/public/portfolio/sade.html +0 -560
  75. package/public/portfolio/sam.html +0 -654
  76. package/public/portfolio/sechar.html +0 -580
  77. package/public/portfolio/sreya.html +0 -599
  78. package/public/portfolio/swen.html +0 -601
  79. /package/dist/src/{ai-hub → core}/brand-store.js +0 -0
@@ -1,449 +0,0 @@
1
- "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.launchDesktopShell = launchDesktopShell;
7
- const electron_1 = require("electron");
8
- const path_1 = __importDefault(require("path"));
9
- const fs_1 = __importDefault(require("fs"));
10
- const server_1 = require("./server");
11
- const cert_store_1 = require("./cert-store");
12
- const office_sideload_1 = require("./office-sideload");
13
- const remote_hub_gateway_1 = require("./remote-hub-gateway");
14
- const project_fraim_paths_1 = require("../core/utils/project-fraim-paths");
15
- const version_utils_1 = require("../cli/utils/version-utils");
16
- const hub_runtime_file_1 = require("./hub-runtime-file");
17
- // ---------------------------------------------------------------------------
18
- // State
19
- // ---------------------------------------------------------------------------
20
- let server = null;
21
- let mainWindow = null;
22
- let tray = null;
23
- let stopping = null;
24
- let isQuitting = false; // distinguishes window-close (→ tray) from app-quit
25
- // ---------------------------------------------------------------------------
26
- // Helpers
27
- // ---------------------------------------------------------------------------
28
- function preferredWindowSize() {
29
- const { workAreaSize } = electron_1.screen.getPrimaryDisplay();
30
- return {
31
- width: Math.max(1440, Math.min(1680, workAreaSize.width)),
32
- height: Math.max(980, Math.min(1180, workAreaSize.height)),
33
- };
34
- }
35
- function parseArgs(argv) {
36
- let projectPath = process.cwd();
37
- let preferredPort = 43091;
38
- for (let i = 0; i < argv.length; i += 1) {
39
- if (argv[i] === '--project-path' && argv[i + 1]) {
40
- projectPath = argv[i + 1];
41
- i += 1;
42
- }
43
- if (argv[i] === '--port' && argv[i + 1]) {
44
- preferredPort = Number(argv[i + 1]) || preferredPort;
45
- i += 1;
46
- }
47
- }
48
- return { projectPath, preferredPort };
49
- }
50
- function applyUserDataOverride() {
51
- const userDataDir = process.env.FRAIM_AI_HUB_USER_DATA_DIR;
52
- if (!userDataDir)
53
- return;
54
- fs_1.default.mkdirSync(userDataDir, { recursive: true });
55
- electron_1.app.setPath('userData', userDataDir);
56
- }
57
- function isTrustedInAppNavigation(targetUrl, hubUrl) {
58
- try {
59
- const parsed = new URL(targetUrl);
60
- if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:')
61
- return false;
62
- return parsed.origin === new URL(hubUrl).origin || parsed.origin === new URL((0, remote_hub_gateway_1.resolveFraimRemoteUrl)()).origin;
63
- }
64
- catch {
65
- return false;
66
- }
67
- }
68
- function connectedSurfaceParam(value) {
69
- return value === 'account' || value === 'analytics' || value === 'brain' ? value : null;
70
- }
71
- function connectedSurfaceForHostedUrl(targetUrl, hubUrl) {
72
- try {
73
- const parsed = new URL(targetUrl);
74
- const hub = new URL(hubUrl);
75
- const remote = new URL((0, remote_hub_gateway_1.resolveFraimRemoteUrl)());
76
- if (parsed.origin === hub.origin || parsed.origin !== remote.origin)
77
- return null;
78
- const pathname = parsed.pathname.replace(/\/+$/, '') || '/';
79
- if (pathname === '/account')
80
- return 'account';
81
- if (pathname === '/analytics')
82
- return 'analytics';
83
- if (pathname === '/fraim-brain' || pathname === '/fraim-brain.html')
84
- return 'brain';
85
- if (pathname === '/ai-hub') {
86
- return connectedSurfaceParam(parsed.searchParams.get('connected') || parsed.searchParams.get('open'));
87
- }
88
- if (pathname === '/auth/sign-in.html' || pathname === '/auth/recovery.html' || pathname === '/auth/error.html') {
89
- return connectedSurfaceParam(parsed.searchParams.get('surface'));
90
- }
91
- return null;
92
- }
93
- catch {
94
- return null;
95
- }
96
- }
97
- function localHubConnectedUrl(hubUrl, surface, sourceUrl) {
98
- const target = new URL(hubUrl);
99
- target.search = '';
100
- target.hash = '';
101
- target.searchParams.set('connected', surface);
102
- if (sourceUrl) {
103
- try {
104
- const source = new URL(sourceUrl);
105
- const apiKey = source.searchParams.get('api-key') || source.searchParams.get('apiKey');
106
- if (apiKey)
107
- target.searchParams.set('api-key', apiKey);
108
- }
109
- catch { /* ignore malformed source URLs */ }
110
- }
111
- return target.toString();
112
- }
113
- function wrappedHostedNavigationUrl(targetUrl, hubUrl) {
114
- const surface = connectedSurfaceForHostedUrl(targetUrl, hubUrl);
115
- return surface ? localHubConnectedUrl(hubUrl, surface, targetUrl) : null;
116
- }
117
- // ---------------------------------------------------------------------------
118
- // Tray icon resolution — prefers bundled icon, falls back to a 1×1 empty image
119
- // so the app never crashes if assets aren't present.
120
- // ---------------------------------------------------------------------------
121
- function resolveTrayIcon() {
122
- const candidates = [
123
- path_1.default.resolve(process.cwd(), 'extensions/office-word/icon-64.png'),
124
- path_1.default.resolve(__dirname, '..', '..', 'extensions/office-word/icon-64.png'),
125
- path_1.default.resolve(__dirname, '..', '..', '..', 'extensions/office-word/icon-64.png'),
126
- ];
127
- for (const c of candidates) {
128
- if (fs_1.default.existsSync(c))
129
- return electron_1.nativeImage.createFromPath(c).resize({ width: 16, height: 16 });
130
- }
131
- return electron_1.nativeImage.createEmpty();
132
- }
133
- // ---------------------------------------------------------------------------
134
- // Login-item (auto-start on boot)
135
- // ---------------------------------------------------------------------------
136
- function ensureLoginItem() {
137
- const flagPath = path_1.default.join(electron_1.app.getPath('userData'), 'login-item-set.flag');
138
- if (fs_1.default.existsSync(flagPath))
139
- return;
140
- electron_1.app.setLoginItemSettings({ openAtLogin: true, openAsHidden: true });
141
- fs_1.default.mkdirSync(path_1.default.dirname(flagPath), { recursive: true });
142
- fs_1.default.writeFileSync(flagPath, '1');
143
- }
144
- // ---------------------------------------------------------------------------
145
- // Word manifest sideload (runs once on first launch)
146
- // ---------------------------------------------------------------------------
147
- function ensureWordSideload(projectPath, httpPort) {
148
- // Flag version bump: bump this string when new manifests are added so all
149
- // users get re-sideloaded on their next launch.
150
- const FLAG_VERSION = 'v4-dynamic-port';
151
- const expectedFlag = `${FLAG_VERSION}:${httpPort}`;
152
- const flagPath = path_1.default.join(electron_1.app.getPath('userData'), 'word-sideloaded.flag');
153
- if (fs_1.default.existsSync(flagPath) && fs_1.default.readFileSync(flagPath, 'utf8').trim() === expectedFlag)
154
- return;
155
- if (httpPort > 0) {
156
- const result = (0, office_sideload_1.sideloadManifest)(projectPath, { httpPort, userDataDir: electron_1.app.getPath('userData') });
157
- if (!result.ok) {
158
- console.warn('[fraim] Office sideload skipped:', result.reason);
159
- return; // don't write flag — retry next launch
160
- }
161
- }
162
- fs_1.default.mkdirSync(path_1.default.dirname(flagPath), { recursive: true });
163
- fs_1.default.writeFileSync(flagPath, expectedFlag);
164
- }
165
- // ---------------------------------------------------------------------------
166
- // Tray setup
167
- // ---------------------------------------------------------------------------
168
- function buildTrayMenu(hubUrl) {
169
- return electron_1.Menu.buildFromTemplate([
170
- {
171
- label: 'Open FRAIM Hub',
172
- click: () => {
173
- if (mainWindow) {
174
- mainWindow.show();
175
- mainWindow.focus();
176
- }
177
- else {
178
- void createWindow(hubUrl);
179
- }
180
- },
181
- },
182
- {
183
- // #755: surface the running build version so staleness is diagnosable at a glance.
184
- label: 'About FRAIM Hub',
185
- click: () => {
186
- void electron_1.dialog.showMessageBox({
187
- type: 'info',
188
- title: 'About FRAIM Hub',
189
- message: 'FRAIM Hub',
190
- detail: `Version ${(0, version_utils_1.getFraimVersion)()}\nIdentity: ~/.fraim/config.json\nLocal server: ${hubUrl}`,
191
- buttons: ['OK'],
192
- });
193
- },
194
- },
195
- { type: 'separator' },
196
- {
197
- label: 'Word Add-in',
198
- enabled: false,
199
- sublabel: (0, office_sideload_1.isSideloaded)() ? 'Active in Word' : 'Not sideloaded',
200
- },
201
- { type: 'separator' },
202
- {
203
- label: 'Start at Login',
204
- type: 'checkbox',
205
- checked: electron_1.app.getLoginItemSettings().openAtLogin,
206
- click: (item) => {
207
- electron_1.app.setLoginItemSettings({ openAtLogin: item.checked, openAsHidden: true });
208
- },
209
- },
210
- { type: 'separator' },
211
- {
212
- label: 'Quit FRAIM',
213
- click: () => {
214
- isQuitting = true;
215
- electron_1.app.quit();
216
- },
217
- },
218
- ]);
219
- }
220
- function createTray(hubUrl) {
221
- tray = new electron_1.Tray(resolveTrayIcon());
222
- tray.setToolTip('FRAIM AI Hub');
223
- tray.setContextMenu(buildTrayMenu(hubUrl));
224
- tray.on('double-click', () => {
225
- if (mainWindow) {
226
- mainWindow.show();
227
- mainWindow.focus();
228
- }
229
- else {
230
- void createWindow(hubUrl);
231
- }
232
- });
233
- }
234
- // ---------------------------------------------------------------------------
235
- // BrowserWindow
236
- // ---------------------------------------------------------------------------
237
- async function createWindow(url) {
238
- const { width, height } = preferredWindowSize();
239
- const isMac = process.platform === 'darwin';
240
- const isWin = process.platform === 'win32';
241
- mainWindow = new electron_1.BrowserWindow({
242
- title: 'FRAIM AI Hub',
243
- width,
244
- height,
245
- minWidth: 1200,
246
- minHeight: 800,
247
- useContentSize: true,
248
- autoHideMenuBar: true,
249
- backgroundColor: (isMac || isWin) ? '#00000000' : '#ECECEC',
250
- titleBarStyle: isMac ? 'hiddenInset' : 'hidden',
251
- ...(isWin && {
252
- titleBarOverlay: {
253
- color: 'rgba(0,0,0,0)',
254
- // Glyphs must contrast the Mica material: light in dark mode, dark in light mode.
255
- symbolColor: electron_1.nativeTheme.shouldUseDarkColors ? '#E8E8E8' : '#1A1A1A',
256
- height: 36,
257
- },
258
- }),
259
- ...(isWin && { backgroundMaterial: 'mica' }),
260
- ...(isMac && { vibrancy: 'sidebar' }),
261
- show: false,
262
- webPreferences: { contextIsolation: true, nodeIntegration: false, sandbox: true },
263
- });
264
- electron_1.Menu.setApplicationMenu(null);
265
- // Keep the Windows titlebar control glyphs readable when the OS theme flips
266
- // between light and dark while the window is open.
267
- if (isWin) {
268
- const applyOverlay = () => {
269
- if (!mainWindow)
270
- return;
271
- try {
272
- mainWindow.setTitleBarOverlay({
273
- color: 'rgba(0,0,0,0)',
274
- symbolColor: electron_1.nativeTheme.shouldUseDarkColors ? '#E8E8E8' : '#1A1A1A',
275
- height: 36,
276
- });
277
- }
278
- catch { /* overlay unsupported on this Windows build */ }
279
- };
280
- electron_1.nativeTheme.on('updated', applyOverlay);
281
- mainWindow.on('closed', () => electron_1.nativeTheme.removeListener('updated', applyOverlay));
282
- }
283
- mainWindow.webContents.setWindowOpenHandler(({ url: targetUrl }) => {
284
- const wrappedUrl = wrappedHostedNavigationUrl(targetUrl, url);
285
- if (wrappedUrl) {
286
- void mainWindow?.loadURL(wrappedUrl);
287
- return { action: 'deny' };
288
- }
289
- if (isTrustedInAppNavigation(targetUrl, url)) {
290
- void mainWindow?.loadURL(targetUrl);
291
- }
292
- return { action: 'deny' };
293
- });
294
- mainWindow.webContents.on('will-navigate', (event, targetUrl) => {
295
- const wrappedUrl = wrappedHostedNavigationUrl(targetUrl, url);
296
- if (!wrappedUrl)
297
- return;
298
- event.preventDefault();
299
- if (mainWindow?.webContents.getURL() !== wrappedUrl) {
300
- void mainWindow?.loadURL(wrappedUrl);
301
- }
302
- });
303
- mainWindow.webContents.on('will-redirect', (details) => {
304
- if (!details.isMainFrame)
305
- return;
306
- const wrappedUrl = wrappedHostedNavigationUrl(details.url, url);
307
- if (!wrappedUrl)
308
- return;
309
- details.preventDefault();
310
- if (mainWindow?.webContents.getURL() !== wrappedUrl) {
311
- void mainWindow?.loadURL(wrappedUrl);
312
- }
313
- });
314
- mainWindow.webContents.on('did-navigate', (_event, targetUrl) => {
315
- const wrappedUrl = wrappedHostedNavigationUrl(targetUrl, url);
316
- if (!wrappedUrl || mainWindow?.webContents.getURL() === wrappedUrl)
317
- return;
318
- void mainWindow?.loadURL(wrappedUrl);
319
- });
320
- mainWindow.once('ready-to-show', () => mainWindow?.show());
321
- // Closing the window hides it to the tray rather than quitting the app.
322
- // The server keeps running so Word can still reach the task pane.
323
- mainWindow.on('close', (event) => {
324
- if (!isQuitting) {
325
- event.preventDefault();
326
- mainWindow?.hide();
327
- }
328
- });
329
- mainWindow.on('closed', () => { mainWindow = null; });
330
- await mainWindow.loadURL(url);
331
- mainWindow.webContents.setZoomFactor(1);
332
- }
333
- // ---------------------------------------------------------------------------
334
- // Server lifecycle
335
- // ---------------------------------------------------------------------------
336
- async function stopServer() {
337
- if (!server)
338
- return;
339
- await server.stop();
340
- server = null;
341
- }
342
- function stopServerOnce() {
343
- if (!stopping) {
344
- stopping = stopServer().finally(() => { stopping = null; });
345
- }
346
- return stopping;
347
- }
348
- // ---------------------------------------------------------------------------
349
- // Launch
350
- // ---------------------------------------------------------------------------
351
- async function launchDesktopShell(options) {
352
- const httpPort = await (0, server_1.findAvailablePort)(options.preferredPort);
353
- const httpsPort = await (0, server_1.findAvailablePortExcluding)(43092, new Set([httpPort]));
354
- // Generate (or load cached) self-signed cert for HTTPS.
355
- // Fast on subsequent launches (file read); ~200ms on first launch (key gen).
356
- const certBundle = await (0, cert_store_1.loadOrCreateCert)();
357
- server = new server_1.AiHubServer({
358
- projectPath: options.projectPath,
359
- // Issue #701: no local DB. Persona/manager-team state resolves through the hosted
360
- // server via the Hub's default remote gateway (authenticated by the user's API key).
361
- httpsPort,
362
- certBundle,
363
- folderPicker: async () => {
364
- const result = await electron_1.dialog.showOpenDialog(mainWindow, {
365
- title: 'Select a FRAIM project folder',
366
- properties: ['openDirectory', 'createDirectory'],
367
- buttonLabel: 'Select Folder',
368
- });
369
- return result.canceled || result.filePaths.length === 0 ? null : result.filePaths[0];
370
- },
371
- });
372
- await server.start(httpPort);
373
- // #755: record the live instance so `fraim hub` can detect a stale build and
374
- // replace it instead of re-focusing an outdated process.
375
- try {
376
- (0, hub_runtime_file_1.writeHubRuntimeFile)((0, project_fraim_paths_1.getUserFraimDirPath)(), {
377
- pid: process.pid,
378
- port: httpPort,
379
- version: (0, version_utils_1.getFraimVersion)(),
380
- startedAt: new Date().toISOString(),
381
- });
382
- }
383
- catch (err) {
384
- console.warn('[fraim] could not write hub-runtime.json:', err);
385
- }
386
- ensureWordSideload(options.projectPath, httpPort);
387
- const hubUrl = `http://127.0.0.1:${httpPort}/ai-hub/`;
388
- createTray(hubUrl);
389
- await createWindow(hubUrl);
390
- }
391
- // ---------------------------------------------------------------------------
392
- // Bootstrap
393
- // ---------------------------------------------------------------------------
394
- async function bootstrap() {
395
- const options = parseArgs(process.argv.slice(2));
396
- applyUserDataOverride();
397
- // Single-instance lock — if another instance is already running, focus it
398
- // and exit rather than spawning a second server + window.
399
- // Skip when FRAIM_AI_HUB_FAKE_HOST=1 (test mode) so Playwright can launch
400
- // a test instance alongside the real desktop app without the lock killing it.
401
- const skipSingleInstance = process.env.FRAIM_AI_HUB_FAKE_HOST === '1';
402
- const gotLock = skipSingleInstance || electron_1.app.requestSingleInstanceLock();
403
- if (!gotLock) {
404
- electron_1.app.quit();
405
- return;
406
- }
407
- electron_1.app.on('second-instance', () => {
408
- if (mainWindow) {
409
- mainWindow.show();
410
- mainWindow.focus();
411
- }
412
- });
413
- await electron_1.app.whenReady();
414
- electron_1.app.setName('FRAIM Hub');
415
- // First-launch housekeeping (idempotent, fast on subsequent runs)
416
- ensureLoginItem();
417
- electron_1.app.on('activate', () => {
418
- // macOS: clicking dock icon re-shows the window
419
- if (mainWindow) {
420
- mainWindow.show();
421
- mainWindow.focus();
422
- }
423
- });
424
- electron_1.app.on('before-quit', () => {
425
- isQuitting = true;
426
- // #755: clear the runtime file so a later `fraim hub` doesn't treat a
427
- // cleanly-exited instance as live.
428
- try {
429
- (0, hub_runtime_file_1.removeHubRuntimeFile)((0, project_fraim_paths_1.getUserFraimDirPath)());
430
- }
431
- catch { /* best-effort */ }
432
- void stopServerOnce();
433
- });
434
- // Keep app alive when all windows are closed — server must keep serving
435
- // for Word to reach the task pane. Only quit on explicit tray → Quit.
436
- electron_1.app.on('window-all-closed', () => {
437
- if (process.platform !== 'darwin' && isQuitting) {
438
- void stopServerOnce().finally(() => electron_1.app.quit());
439
- }
440
- });
441
- await launchDesktopShell(options);
442
- }
443
- if (process.versions.electron && process.type !== 'renderer') {
444
- bootstrap().catch(async (error) => {
445
- console.error(error instanceof Error ? error.message : error);
446
- await stopServerOnce();
447
- electron_1.app.exit(1);
448
- });
449
- }