antri_cli 1.45.0 → 1.47.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.
@@ -16,6 +16,7 @@ let skillSearchQuery = '';
16
16
 
17
17
  // Initialize on Load
18
18
  document.addEventListener('DOMContentLoaded', async () => {
19
+ await checkAuthStatus();
19
20
  await loadStatus();
20
21
  await loadCommands();
21
22
  await loadProfiles();
@@ -455,7 +456,24 @@ async function submitPrompt() {
455
456
  if (line.startsWith('data: ')) {
456
457
  try {
457
458
  const data = JSON.parse(line.slice(6));
458
- if (data.token) {
459
+ if (data.requestId && data.name && data.args !== undefined) {
460
+ // Interactive Permission Request Card
461
+ const permCard = document.createElement('div');
462
+ permCard.className = 'permission-prompt-card';
463
+ permCard.id = `card-${data.requestId}`;
464
+ permCard.innerHTML = `
465
+ <div class="perm-title">⚠️ Privacy & Security Permission Request</div>
466
+ <div class="perm-desc">Agent requested to execute tool: <b style="color:var(--accent-primary);">${data.name}</b></div>
467
+ <pre class="perm-args">${JSON.stringify(data.args, null, 2)}</pre>
468
+ <div class="perm-actions">
469
+ <button class="perm-btn perm-allow" onclick="respondDesktopPermission('${data.requestId}', true, false)">Allow Once</button>
470
+ <button class="perm-btn perm-always" onclick="respondDesktopPermission('${data.requestId}', true, true)">Always Allow</button>
471
+ <button class="perm-btn perm-deny" onclick="respondDesktopPermission('${data.requestId}', false, false)">Deny</button>
472
+ </div>
473
+ `;
474
+ assistantMsgEl.insertBefore(permCard, contentEl);
475
+ scrollToBottom();
476
+ } else if (data.token) {
459
477
  accumulated += data.token;
460
478
  contentEl.textContent = accumulated;
461
479
  scrollToBottom();
@@ -479,6 +497,26 @@ async function submitPrompt() {
479
497
  }
480
498
  }
481
499
 
500
+ async function respondDesktopPermission(requestId, allowed, alwaysAllow) {
501
+ const card = document.getElementById(`card-${requestId}`);
502
+ if (card) {
503
+ card.innerHTML = `<div style="font-size:12px;font-weight:600;color:${allowed ? '#10b981' : '#f43f5e'};padding:4px 0;">${allowed ? '✓ Permission granted by user.' : '✕ Permission denied by user.'}</div>`;
504
+ }
505
+ try {
506
+ await fetch('/api/permission/response', {
507
+ method: 'POST',
508
+ headers: { 'Content-Type': 'application/json' },
509
+ body: JSON.stringify({ requestId, allowed, alwaysAllow }),
510
+ });
511
+ if (alwaysAllow) {
512
+ showToast('Permissions set to Always-Allow.');
513
+ await loadStatus();
514
+ }
515
+ } catch (e) {
516
+ console.error('Failed to submit permission response:', e);
517
+ }
518
+ }
519
+
482
520
  function appendMessage(role, text) {
483
521
  const container = document.getElementById('chat-messages');
484
522
  const row = document.createElement('div');
@@ -688,6 +726,37 @@ async function saveActiveProfile() {
688
726
  showToast('Profile saved successfully.');
689
727
  }
690
728
 
729
+ async function pushProfilesToCloud() {
730
+ try {
731
+ showToast('Pushing profiles to Google Cloud Firestore...');
732
+ const res = await fetch('/api/profile/push', { method: 'POST' });
733
+ const data = await res.json();
734
+ if (data.success) {
735
+ showToast(`Pushed ${data.count} profile(s) to Google Cloud Firestore.`);
736
+ } else {
737
+ showToast(`Push failed: ${data.error || 'Check network connection'}`, true);
738
+ }
739
+ } catch (err) {
740
+ showToast(`Push failed: ${err.message}`, true);
741
+ }
742
+ }
743
+
744
+ async function pullProfilesFromCloud() {
745
+ try {
746
+ showToast('Pulling profiles from Google Cloud Firestore...');
747
+ const res = await fetch('/api/profile/pull', { method: 'POST' });
748
+ const data = await res.json();
749
+ if (data.success) {
750
+ await loadProfiles();
751
+ showToast(`Pulled ${data.count} profile(s) from Google Cloud Firestore.`);
752
+ } else {
753
+ showToast(`Pull failed: ${data.error || 'Check network connection'}`, true);
754
+ }
755
+ } catch (err) {
756
+ showToast(`Pull failed: ${err.message}`, true);
757
+ }
758
+ }
759
+
691
760
  function exportActiveProfile() {
692
761
  const activeTitle = document.getElementById('active-profile-title').textContent || 'profile.md';
693
762
  const content = document.getElementById('profile-editor').value;
@@ -953,3 +1022,96 @@ async function loadMemory() {
953
1022
  console.error('Failed to load memory:', err);
954
1023
  }
955
1024
  }
1025
+
1026
+ // Authentication Helpers
1027
+ let currentAuthUser = null;
1028
+
1029
+ async function checkAuthStatus() {
1030
+ try {
1031
+ const res = await fetch('/api/auth/status');
1032
+ const data = await res.json();
1033
+ const dot = document.getElementById('auth-status-dot');
1034
+ const text = document.getElementById('auth-status-text');
1035
+
1036
+ if (data.isAuthenticated && data.user) {
1037
+ currentAuthUser = data.user;
1038
+ if (dot) dot.classList.add('logged-in');
1039
+ if (text) text.textContent = data.user.email.split('@')[0];
1040
+ } else {
1041
+ currentAuthUser = null;
1042
+ if (dot) dot.classList.remove('logged-in');
1043
+ if (text) text.textContent = 'Login';
1044
+ }
1045
+ } catch (e) {
1046
+ console.error('Failed to check auth status:', e);
1047
+ }
1048
+ }
1049
+
1050
+ function openAuthModal() {
1051
+ const modal = document.getElementById('auth-modal');
1052
+ const formView = document.getElementById('auth-form-view');
1053
+ const loggedView = document.getElementById('auth-logged-view');
1054
+ const emailText = document.getElementById('logged-user-email');
1055
+ const idText = document.getElementById('logged-user-id');
1056
+
1057
+ if (currentAuthUser) {
1058
+ if (formView) formView.style.display = 'none';
1059
+ if (loggedView) loggedView.style.display = 'block';
1060
+ if (emailText) emailText.textContent = currentAuthUser.email;
1061
+ if (idText) idText.textContent = `Cloud Partition: ${currentAuthUser.userId}`;
1062
+ } else {
1063
+ if (formView) formView.style.display = 'block';
1064
+ if (loggedView) loggedView.style.display = 'none';
1065
+ }
1066
+
1067
+ if (modal) modal.style.display = 'flex';
1068
+ }
1069
+
1070
+ function closeAuthModal() {
1071
+ const modal = document.getElementById('auth-modal');
1072
+ if (modal) modal.style.display = 'none';
1073
+ }
1074
+
1075
+ async function submitDesktopLogin() {
1076
+ const emailInput = document.getElementById('modal-email-input');
1077
+ const passInput = document.getElementById('modal-pass-input');
1078
+ const email = (emailInput ? emailInput.value : '').trim();
1079
+ const password = passInput ? passInput.value : '';
1080
+
1081
+ if (!email || !email.includes('@')) {
1082
+ alert('Please enter a valid email address.');
1083
+ return;
1084
+ }
1085
+
1086
+ try {
1087
+ const res = await fetch('/api/auth/login', {
1088
+ method: 'POST',
1089
+ headers: { 'Content-Type': 'application/json' },
1090
+ body: JSON.stringify({ email, password }),
1091
+ });
1092
+ const data = await res.json();
1093
+ if (data.success && data.user) {
1094
+ currentAuthUser = data.user;
1095
+ closeAuthModal();
1096
+ await checkAuthStatus();
1097
+ await loadProfiles();
1098
+ showToast(`Logged in as ${data.user.email}`);
1099
+ } else {
1100
+ alert(data.error || 'Login failed.');
1101
+ }
1102
+ } catch (err) {
1103
+ alert('Login error: ' + err.message);
1104
+ }
1105
+ }
1106
+
1107
+ async function submitDesktopLogout() {
1108
+ try {
1109
+ await fetch('/api/auth/logout', { method: 'POST' });
1110
+ currentAuthUser = null;
1111
+ closeAuthModal();
1112
+ await checkAuthStatus();
1113
+ showToast('Logged out of ANTRI.');
1114
+ } catch (err) {
1115
+ console.error('Logout error:', err);
1116
+ }
1117
+ }
@@ -15,7 +15,7 @@
15
15
  <header class="app-header">
16
16
  <div class="brand-zone">
17
17
  <span class="logo-mark">ANTRI</span>
18
- <span class="version-tag">v1.45.0</span>
18
+ <span class="version-tag">v1.47.0</span>
19
19
  </div>
20
20
 
21
21
  <!-- Mode Toggle -->
@@ -63,6 +63,11 @@
63
63
  <button id="btn-perms-toggle" class="perms-badge" onclick="toggleAlwaysAllow()">
64
64
  <span id="perms-text">Ask-First</span>
65
65
  </button>
66
+
67
+ <button id="btn-auth-status" class="auth-header-btn" onclick="openAuthModal()">
68
+ <span id="auth-status-dot" class="status-dot"></span>
69
+ <span id="auth-status-text">Login</span>
70
+ </button>
66
71
  </div>
67
72
  </header>
68
73
 
@@ -276,6 +281,8 @@
276
281
  <input type="text" id="new-profile-name" placeholder="New profile name (e.g. backend_architect)" />
277
282
  <button class="action-btn" onclick="createProfile()">+ Create</button>
278
283
  <button class="action-btn-secondary" onclick="document.getElementById('profile-import-input').click()">📁 Import .md</button>
284
+ <button class="action-btn-secondary" onclick="pushProfilesToCloud()">☁️ Push</button>
285
+ <button class="action-btn-secondary" onclick="pullProfilesFromCloud()">☁️ Pull</button>
279
286
  </div>
280
287
  </div>
281
288
 
@@ -378,6 +385,36 @@
378
385
  </main>
379
386
  </div>
380
387
 
388
+ <!-- AUTH MODAL -->
389
+ <div id="auth-modal" class="modal-backdrop" style="display:none;" onclick="if(event.target===this)closeAuthModal()">
390
+ <div class="modal-box">
391
+ <div class="modal-header">
392
+ <h3 id="modal-auth-title">ANTRI Account</h3>
393
+ <button class="modal-close" onclick="closeAuthModal()">✕</button>
394
+ </div>
395
+ <div class="modal-body" id="modal-auth-body">
396
+ <div id="auth-form-view">
397
+ <p style="font-size:13px;color:var(--text-muted);margin-bottom:16px;">Sign in to sync your thinking profiles and memories across CLI, Desktop, and Mobile.</p>
398
+ <div style="margin-bottom:12px;">
399
+ <label style="display:block;font-size:12px;font-weight:600;margin-bottom:6px;">Email Address</label>
400
+ <input type="email" id="modal-email-input" class="modal-input" placeholder="user@gmail.com" />
401
+ </div>
402
+ <div style="margin-bottom:16px;">
403
+ <label style="display:block;font-size:12px;font-weight:600;margin-bottom:6px;">Password</label>
404
+ <input type="password" id="modal-pass-input" class="modal-input" placeholder="••••••••" />
405
+ </div>
406
+ <button class="action-btn" style="width:100%;margin-bottom:8px;" onclick="submitDesktopLogin()">Sign In / Register</button>
407
+ </div>
408
+ <div id="auth-logged-view" style="display:none;text-align:center;padding:12px 0;">
409
+ <div style="font-size:32px;margin-bottom:8px;">👤</div>
410
+ <div id="logged-user-email" style="font-weight:700;font-size:15px;color:var(--primary);margin-bottom:4px;"></div>
411
+ <div id="logged-user-id" style="font-size:12px;color:var(--text-muted);margin-bottom:16px;"></div>
412
+ <button class="action-btn-danger" style="width:100%;" onclick="submitDesktopLogout()">Logout</button>
413
+ </div>
414
+ </div>
415
+ </div>
416
+ </div>
417
+
381
418
  <script src="app.js"></script>
382
419
  </body>
383
420
  </html>
@@ -1146,3 +1146,176 @@ body.antri-app {
1146
1146
  transform: translateY(0);
1147
1147
  }
1148
1148
 
1149
+ /* Auth Header Button & Modal */
1150
+ .auth-header-btn {
1151
+ background: var(--bg-surface);
1152
+ border: 1px solid var(--border-color);
1153
+ color: var(--text-primary);
1154
+ padding: 0.35rem 0.75rem;
1155
+ border-radius: var(--radius-xs);
1156
+ font-size: 0.8rem;
1157
+ font-weight: 600;
1158
+ display: inline-flex;
1159
+ align-items: center;
1160
+ gap: 6px;
1161
+ cursor: pointer;
1162
+ transition: all 0.15s ease;
1163
+ }
1164
+
1165
+ .auth-header-btn:hover {
1166
+ background: var(--bg-surface-elevated);
1167
+ border-color: var(--accent-primary);
1168
+ }
1169
+
1170
+ .status-dot {
1171
+ width: 7px;
1172
+ height: 7px;
1173
+ border-radius: 50%;
1174
+ background: #f43f5e;
1175
+ }
1176
+
1177
+ .status-dot.logged-in {
1178
+ background: #10b981;
1179
+ }
1180
+
1181
+ .modal-backdrop {
1182
+ position: fixed;
1183
+ inset: 0;
1184
+ background: rgba(0, 0, 0, 0.6);
1185
+ backdrop-filter: blur(4px);
1186
+ display: flex;
1187
+ align-items: center;
1188
+ justify-content: center;
1189
+ z-index: 9999;
1190
+ }
1191
+
1192
+ .modal-box {
1193
+ background: var(--bg-surface-elevated, #18181b);
1194
+ border: 1px solid var(--border-color, #27272a);
1195
+ border-radius: 12px;
1196
+ width: 100%;
1197
+ max-width: 400px;
1198
+ padding: 24px;
1199
+ box-shadow: 0 10px 30px rgba(0, 0, 0, 0.4);
1200
+ }
1201
+
1202
+ .modal-header {
1203
+ display: flex;
1204
+ justify-content: space-between;
1205
+ align-items: center;
1206
+ margin-bottom: 16px;
1207
+ }
1208
+
1209
+ .modal-close {
1210
+ background: transparent;
1211
+ border: none;
1212
+ color: var(--text-tertiary);
1213
+ font-size: 16px;
1214
+ cursor: pointer;
1215
+ }
1216
+
1217
+ .modal-close:hover {
1218
+ color: var(--text-primary);
1219
+ }
1220
+
1221
+ .modal-input {
1222
+ width: 100%;
1223
+ padding: 10px 12px;
1224
+ background: var(--bg-surface, #09090b);
1225
+ border: 1px solid var(--border-color, #27272a);
1226
+ border-radius: 6px;
1227
+ color: var(--text-primary, #f4f4f5);
1228
+ font-size: 13px;
1229
+ outline: none;
1230
+ }
1231
+
1232
+ .modal-input:focus {
1233
+ border-color: var(--accent-primary, #6366f1);
1234
+ }
1235
+
1236
+ .action-btn-danger {
1237
+ background: #dc2626;
1238
+ color: #fff;
1239
+ border: none;
1240
+ padding: 8px 16px;
1241
+ border-radius: 6px;
1242
+ font-weight: 600;
1243
+ font-size: 13px;
1244
+ cursor: pointer;
1245
+ }
1246
+
1247
+ .action-btn-danger:hover {
1248
+ background: #b91c1c;
1249
+ }
1250
+
1251
+ /* Tool Permission Prompt Card */
1252
+ .permission-prompt-card {
1253
+ background: var(--bg-surface-elevated, #18181b);
1254
+ border: 1px solid #f59e0b;
1255
+ border-radius: 8px;
1256
+ padding: 12px 14px;
1257
+ margin: 10px 0;
1258
+ box-shadow: 0 4px 12px rgba(245, 158, 11, 0.1);
1259
+ }
1260
+
1261
+ .perm-title {
1262
+ font-size: 12px;
1263
+ font-weight: 700;
1264
+ color: #f59e0b;
1265
+ margin-bottom: 4px;
1266
+ }
1267
+
1268
+ .perm-desc {
1269
+ font-size: 13px;
1270
+ color: var(--text-primary);
1271
+ margin-bottom: 8px;
1272
+ }
1273
+
1274
+ .perm-args {
1275
+ background: var(--bg-surface, #09090b);
1276
+ border: 1px solid var(--border-color);
1277
+ padding: 8px 10px;
1278
+ border-radius: 6px;
1279
+ font-family: var(--font-mono);
1280
+ font-size: 11px;
1281
+ max-height: 120px;
1282
+ overflow-y: auto;
1283
+ margin-bottom: 10px;
1284
+ color: #38bdf8;
1285
+ }
1286
+
1287
+ .perm-actions {
1288
+ display: flex;
1289
+ gap: 8px;
1290
+ }
1291
+
1292
+ .perm-btn {
1293
+ padding: 6px 12px;
1294
+ border-radius: 4px;
1295
+ font-size: 12px;
1296
+ font-weight: 600;
1297
+ cursor: pointer;
1298
+ border: none;
1299
+ transition: opacity 0.15s;
1300
+ }
1301
+
1302
+ .perm-btn:hover {
1303
+ opacity: 0.9;
1304
+ }
1305
+
1306
+ .perm-allow {
1307
+ background: #10b981;
1308
+ color: #fff;
1309
+ }
1310
+
1311
+ .perm-always {
1312
+ background: #6366f1;
1313
+ color: #fff;
1314
+ }
1315
+
1316
+ .perm-deny {
1317
+ background: #ef4444;
1318
+ color: #fff;
1319
+ }
1320
+
1321
+
@@ -2,7 +2,10 @@ export declare class DesktopServer {
2
2
  private server;
3
3
  private port;
4
4
  private activeAgent;
5
+ private pendingPermissions;
6
+ private currentSseSender;
5
7
  constructor();
8
+ private setupPermissionHandler;
6
9
  start(): Promise<number>;
7
10
  stop(): Promise<void>;
8
11
  private handleApi;
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/desktop/server.ts"],"names":[],"mappings":"AAqCA,qBAAa,aAAa;IACxB,OAAO,CAAC,MAAM,CAA4B;IAC1C,OAAO,CAAC,IAAI,CAAQ;IACpB,OAAO,CAAC,WAAW,CAAa;;IAMnB,KAAK,IAAI,OAAO,CAAC,MAAM,CAAC;IAqExB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;YAUpB,SAAS;WA4XH,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC;CAyBnD"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/desktop/server.ts"],"names":[],"mappings":"AAqCA,qBAAa,aAAa;IACxB,OAAO,CAAC,MAAM,CAA4B;IAC1C,OAAO,CAAC,IAAI,CAAQ;IACpB,OAAO,CAAC,WAAW,CAAa;IAChC,OAAO,CAAC,kBAAkB,CAAiD;IAC3E,OAAO,CAAC,gBAAgB,CAAqD;;IAO7E,OAAO,CAAC,sBAAsB;IA2BjB,KAAK,IAAI,OAAO,CAAC,MAAM,CAAC;IAqExB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;YAUpB,SAAS;WA2aH,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC;CAyBnD"}
@@ -12,7 +12,7 @@ import { profileManager } from '../profiles/profileManager.js';
12
12
  import { memoryManager } from '../memory/manager.js';
13
13
  import { skillManager } from '../skills/skillManager.js';
14
14
  import { SkillSynthesizer } from '../core/skillSynthesizer.js';
15
- import { getAllActiveTools } from '../core/tools.js';
15
+ import { getAllActiveTools, ToolExecutor } from '../core/tools.js';
16
16
  import { getAvailableModels } from '../providers/models.js';
17
17
  import { PROMPT_TOOLKIT_COMMANDS } from '../cli/promptToolkit.js';
18
18
  import { FilePickerService } from '../cli/dialogs/filePicker.js';
@@ -36,8 +36,37 @@ export class DesktopServer {
36
36
  server = null;
37
37
  port = 3456;
38
38
  activeAgent;
39
+ pendingPermissions = new Map();
40
+ currentSseSender = null;
39
41
  constructor() {
40
42
  this.activeAgent = new AntriAgent(configManager.get());
43
+ this.setupPermissionHandler();
44
+ }
45
+ setupPermissionHandler() {
46
+ this.activeAgent.getToolExecutor().setPermissionHandler(async (name, args) => {
47
+ const cfg = configManager.get();
48
+ if (cfg.alwaysAllow)
49
+ return true;
50
+ if (!ToolExecutor.isSensitive(name))
51
+ return true;
52
+ const reqId = `perm_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`;
53
+ if (this.currentSseSender) {
54
+ this.currentSseSender('permission_request', {
55
+ requestId: reqId,
56
+ name,
57
+ args,
58
+ });
59
+ }
60
+ return new Promise((resolve) => {
61
+ this.pendingPermissions.set(reqId, resolve);
62
+ setTimeout(() => {
63
+ if (this.pendingPermissions.has(reqId)) {
64
+ this.pendingPermissions.delete(reqId);
65
+ resolve(false);
66
+ }
67
+ }, 90000);
68
+ });
69
+ });
41
70
  }
42
71
  async start() {
43
72
  const publicDir = getPublicDir();
@@ -213,6 +242,22 @@ export class DesktopServer {
213
242
  }));
214
243
  return;
215
244
  }
245
+ // POST /api/permission/response (Desktop tool confirmation response)
246
+ if (pathname === '/api/permission/response' && req.method === 'POST') {
247
+ const { requestId, allowed, alwaysAllow } = payload;
248
+ if (alwaysAllow) {
249
+ configManager.setAlwaysAllow(true);
250
+ this.activeAgent.updateConfig(configManager.get());
251
+ }
252
+ const resolver = this.pendingPermissions.get(requestId);
253
+ if (resolver) {
254
+ this.pendingPermissions.delete(requestId);
255
+ resolver(!!allowed);
256
+ }
257
+ res.writeHead(200, { 'Content-Type': 'application/json' });
258
+ res.end(JSON.stringify({ success: true }));
259
+ return;
260
+ }
216
261
  // POST /api/chat (Real-Time SSE Token Streaming)
217
262
  if (pathname === '/api/chat' && req.method === 'POST') {
218
263
  res.writeHead(200, {
@@ -223,6 +268,7 @@ export class DesktopServer {
223
268
  const sendEvent = (event, data) => {
224
269
  res.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
225
270
  };
271
+ this.currentSseSender = sendEvent;
226
272
  const userPrompt = payload.prompt || '';
227
273
  this.activeAgent.updateConfig(configManager.get());
228
274
  try {
@@ -241,6 +287,9 @@ export class DesktopServer {
241
287
  sendEvent('error', { message: err.message });
242
288
  res.end();
243
289
  }
290
+ finally {
291
+ this.currentSseSender = null;
292
+ }
244
293
  return;
245
294
  }
246
295
  // POST /api/debate (SSE Streaming)
@@ -337,6 +386,14 @@ export class DesktopServer {
337
386
  res.end(JSON.stringify({ success: true }));
338
387
  return;
339
388
  }
389
+ // GET/POST /api/auth/status
390
+ if (pathname === '/api/auth/status') {
391
+ const { AuthManager } = await import('../cloud/auth.js');
392
+ const user = AuthManager.getCurrentUser();
393
+ res.writeHead(200, { 'Content-Type': 'application/json' });
394
+ res.end(JSON.stringify({ isAuthenticated: !!user, user }));
395
+ return;
396
+ }
340
397
  // POST /api/auth/login
341
398
  if (pathname === '/api/auth/login' && req.method === 'POST') {
342
399
  const { AuthManager } = await import('../cloud/auth.js');
@@ -391,6 +448,22 @@ export class DesktopServer {
391
448
  res.end(JSON.stringify({ success: ok }));
392
449
  return;
393
450
  }
451
+ // POST /api/profile/push
452
+ if (pathname === '/api/profile/push' && req.method === 'POST') {
453
+ const { FirestoreSyncManager } = await import('../cloud/firestore.js');
454
+ const result = await FirestoreSyncManager.pushToFirestore();
455
+ res.writeHead(200, { 'Content-Type': 'application/json' });
456
+ res.end(JSON.stringify(result));
457
+ return;
458
+ }
459
+ // POST /api/profile/pull
460
+ if (pathname === '/api/profile/pull' && req.method === 'POST') {
461
+ const { FirestoreSyncManager } = await import('../cloud/firestore.js');
462
+ const result = await FirestoreSyncManager.pullFromFirestore();
463
+ res.writeHead(200, { 'Content-Type': 'application/json' });
464
+ res.end(JSON.stringify(result));
465
+ return;
466
+ }
394
467
  // POST /api/skill/create
395
468
  if (pathname === '/api/skill/create' && req.method === 'POST') {
396
469
  const skill = skillManager.createSkill(payload.name, payload.description || '', payload.category || 'Custom', payload.triggers || [], payload.content);