aimeat 1.3.2 → 1.3.5

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 (49) hide show
  1. package/README.md +209 -95
  2. package/dist/src/auth/jwt.d.ts +4 -0
  3. package/dist/src/auth/jwt.d.ts.map +1 -1
  4. package/dist/src/auth/jwt.js +5 -0
  5. package/dist/src/auth/jwt.js.map +1 -1
  6. package/dist/src/auth/node-keys.d.ts.map +1 -1
  7. package/dist/src/auth/node-keys.js +4 -1
  8. package/dist/src/auth/node-keys.js.map +1 -1
  9. package/dist/src/mcp/apps.d.ts.map +1 -1
  10. package/dist/src/mcp/apps.js +35 -12
  11. package/dist/src/mcp/apps.js.map +1 -1
  12. package/dist/src/mcp/core.d.ts.map +1 -1
  13. package/dist/src/mcp/core.js +38 -2
  14. package/dist/src/mcp/core.js.map +1 -1
  15. package/dist/src/mcp/cortex.d.ts.map +1 -1
  16. package/dist/src/mcp/cortex.js +37 -6
  17. package/dist/src/mcp/cortex.js.map +1 -1
  18. package/dist/src/mcp/extensions.d.ts.map +1 -1
  19. package/dist/src/mcp/extensions.js +37 -3
  20. package/dist/src/mcp/extensions.js.map +1 -1
  21. package/dist/src/routes/apps.d.ts.map +1 -1
  22. package/dist/src/routes/apps.js +25 -3
  23. package/dist/src/routes/apps.js.map +1 -1
  24. package/dist/src/routes/extensions.d.ts.map +1 -1
  25. package/dist/src/routes/extensions.js +3 -1
  26. package/dist/src/routes/extensions.js.map +1 -1
  27. package/dist/src/routes/storage-files.d.ts.map +1 -1
  28. package/dist/src/routes/storage-files.js +26 -1
  29. package/dist/src/routes/storage-files.js.map +1 -1
  30. package/dist/src/routes/upload.d.ts +22 -0
  31. package/dist/src/routes/upload.d.ts.map +1 -0
  32. package/dist/src/routes/upload.js +219 -0
  33. package/dist/src/routes/upload.js.map +1 -0
  34. package/dist/src/server-bootstrap/routes-loader.d.ts.map +1 -1
  35. package/dist/src/server-bootstrap/routes-loader.js +3 -0
  36. package/dist/src/server-bootstrap/routes-loader.js.map +1 -1
  37. package/dist/src/server.d.ts.map +1 -1
  38. package/dist/src/server.js +11 -3
  39. package/dist/src/server.js.map +1 -1
  40. package/dist/src/services/upload-token.d.ts +37 -0
  41. package/dist/src/services/upload-token.d.ts.map +1 -0
  42. package/dist/src/services/upload-token.js +98 -0
  43. package/dist/src/services/upload-token.js.map +1 -0
  44. package/dist/src/services/upload-zip.d.ts +31 -0
  45. package/dist/src/services/upload-zip.d.ts.map +1 -0
  46. package/dist/src/services/upload-zip.js +236 -0
  47. package/dist/src/services/upload-zip.js.map +1 -0
  48. package/dist/static/app-catalog.html +170 -16
  49. package/package.json +2 -2
@@ -0,0 +1,236 @@
1
+ /**
2
+ * @file upload-zip.ts
3
+ * @description ZIP parsing for extension and cortex uploads via presigned URL.
4
+ * Validates ZIP structure, extracts manifest + scripts/libs, applies security checks
5
+ * (magic bytes, zip bomb detection, path traversal prevention).
6
+ * @structure
7
+ * - parseExtensionZip() — ZIP buffer -> ExtensionRecord
8
+ * - parseCortexZip() — ZIP buffer -> CortexExtensionRecord + libs
9
+ * - extractZipEntries() — internal secure ZIP extraction
10
+ * @usage
11
+ * import { parseExtensionZip, parseCortexZip } from '../services/upload-zip.js';
12
+ * @version-history
13
+ * v1.0.0 — 2026-05-02 — Initial implementation
14
+ */
15
+ import yauzl from 'yauzl';
16
+ import YAML from 'yaml';
17
+ import { parseCortexManifest } from './cortex-manifest.js';
18
+ const ZIP_MAGIC = Buffer.from([0x50, 0x4b, 0x03, 0x04]);
19
+ const MAX_FILES = 50;
20
+ const MAX_DEPTH = 5;
21
+ const DECOMPRESSION_RATIO = 10;
22
+ export async function parseExtensionZip(buffer, config) {
23
+ if (buffer.length < 4 || !buffer.subarray(0, 4).equals(ZIP_MAGIC)) {
24
+ return { ok: false, error: 'Not a valid ZIP file (missing magic bytes)' };
25
+ }
26
+ const maxBytes = config.extensionMaxCodeSizeKb * 1024 * MAX_FILES;
27
+ if (buffer.length > maxBytes) {
28
+ return { ok: false, error: `ZIP exceeds maximum size of ${Math.round(maxBytes / 1024)}KB` };
29
+ }
30
+ let files;
31
+ try {
32
+ files = await extractZipEntries(buffer);
33
+ }
34
+ catch (err) {
35
+ return { ok: false, error: `ZIP extraction failed: ${err.message}` };
36
+ }
37
+ const manifestBuf = files.get('manifest.yaml');
38
+ if (!manifestBuf) {
39
+ return { ok: false, error: 'ZIP must contain manifest.yaml at root' };
40
+ }
41
+ let manifest;
42
+ try {
43
+ manifest = YAML.parse(manifestBuf.toString('utf-8'));
44
+ }
45
+ catch {
46
+ return { ok: false, error: 'manifest.yaml is not valid YAML' };
47
+ }
48
+ const metadata = manifest.metadata;
49
+ if (!metadata?.name || !metadata?.version || !metadata?.description || !metadata?.author) {
50
+ return { ok: false, error: 'metadata.name, metadata.version, metadata.description, and metadata.author are required' };
51
+ }
52
+ const actions = manifest.actions;
53
+ if (!Array.isArray(actions) || actions.length === 0) {
54
+ return { ok: false, error: 'actions array is required and must not be empty' };
55
+ }
56
+ // Extract scripts from scripts/ directory
57
+ const scripts = {};
58
+ for (const [path, buf] of files) {
59
+ if (path.startsWith('scripts/') && path.length > 'scripts/'.length) {
60
+ const filename = path.slice('scripts/'.length);
61
+ scripts[filename] = buf.toString('utf-8');
62
+ }
63
+ }
64
+ for (const action of actions) {
65
+ if (!action.id || !action.method || !action.path || !action.script) {
66
+ return { ok: false, error: 'Each action must have id, method, path, and script fields' };
67
+ }
68
+ if (!scripts[action.script]) {
69
+ return { ok: false, error: `Script "${action.script}" referenced in action "${action.id}" not found in scripts/ directory` };
70
+ }
71
+ }
72
+ for (const [filename, content] of Object.entries(scripts)) {
73
+ const sizeKb = Buffer.byteLength(content, 'utf-8') / 1024;
74
+ if (sizeKb > config.extensionMaxCodeSizeKb) {
75
+ return { ok: false, error: `Script "${filename}" (${Math.round(sizeKb)}KB) exceeds limit of ${config.extensionMaxCodeSizeKb}KB` };
76
+ }
77
+ }
78
+ const name = metadata.name;
79
+ const manifestConfig = manifest.config;
80
+ const manifestLimits = manifest.limits;
81
+ const manifestFederation = manifest.federation;
82
+ const manifestSchedules = manifest.schedules;
83
+ const manifestInstances = manifest.instances;
84
+ const record = {
85
+ name,
86
+ version: metadata.version,
87
+ description: metadata.description,
88
+ author: metadata.author,
89
+ status: 'inactive',
90
+ requiredApis: manifest.required_apis ?? [],
91
+ actions: actions.map(a => ({
92
+ id: a.id,
93
+ method: a.method.toUpperCase(),
94
+ path: a.path,
95
+ inputSchema: a.input ?? {},
96
+ outputSchema: a.output ?? {},
97
+ scriptContent: scripts[a.script],
98
+ })),
99
+ config: {
100
+ ...(manifestConfig
101
+ ? Object.fromEntries(Object.entries(manifestConfig).map(([k, v]) => {
102
+ if (v && typeof v === 'object' && 'default' in v) {
103
+ return [k, v.default];
104
+ }
105
+ return [k, v];
106
+ }))
107
+ : {}),
108
+ ...(manifestSchedules ? { __schedules: manifestSchedules } : {}),
109
+ },
110
+ limits: {
111
+ memoryMb: Math.min(manifestLimits?.memory_mb ?? config.extensionMaxMemoryMb, config.extensionMaxMemoryMb),
112
+ timeoutMs: Math.min(manifestLimits?.timeout_ms ?? config.extensionTimeoutMs, config.extensionTimeoutMs),
113
+ maxApiCalls: Math.min(manifestLimits?.max_api_calls ?? config.extensionMaxApiCalls, config.extensionMaxApiCalls),
114
+ },
115
+ federation: {
116
+ advertise: manifestFederation?.advertise ?? false,
117
+ capabilities: manifestFederation?.capabilities ?? [],
118
+ },
119
+ ...(manifestInstances?.supported ? {
120
+ instances: {
121
+ supported: true,
122
+ configSchema: manifestInstances.config_per_instance ?? undefined,
123
+ },
124
+ } : {}),
125
+ installedBy: 'upload',
126
+ installedAt: new Date().toISOString(),
127
+ };
128
+ return { ok: true, record };
129
+ }
130
+ export async function parseCortexZip(buffer, config, ownerName) {
131
+ if (buffer.length < 4 || !buffer.subarray(0, 4).equals(ZIP_MAGIC)) {
132
+ return { ok: false, error: 'Not a valid ZIP file (missing magic bytes)' };
133
+ }
134
+ const maxBytes = config.cortexMaxLibSizeKb * 1024 * MAX_FILES;
135
+ if (buffer.length > maxBytes) {
136
+ return { ok: false, error: 'ZIP exceeds maximum size' };
137
+ }
138
+ let files;
139
+ try {
140
+ files = await extractZipEntries(buffer);
141
+ }
142
+ catch (err) {
143
+ return { ok: false, error: `ZIP extraction failed: ${err.message}` };
144
+ }
145
+ const manifestBuf = files.get('manifest.yaml');
146
+ if (!manifestBuf) {
147
+ return { ok: false, error: 'ZIP must contain manifest.yaml at root' };
148
+ }
149
+ const manifestStr = manifestBuf.toString('utf-8');
150
+ const manifestSizeKb = Buffer.byteLength(manifestStr, 'utf-8') / 1024;
151
+ if (manifestSizeKb > config.cortexMaxLibSizeKb) {
152
+ return { ok: false, error: `Manifest size ${Math.round(manifestSizeKb)}KB exceeds limit of ${config.cortexMaxLibSizeKb}KB` };
153
+ }
154
+ // Extract libs from libs/ directory
155
+ const libs = {};
156
+ for (const [path, buf] of files) {
157
+ if (path.startsWith('libs/') && path.length > 'libs/'.length) {
158
+ const filename = path.slice('libs/'.length);
159
+ const content = buf.toString('utf-8');
160
+ const sizeKb = Buffer.byteLength(content, 'utf-8') / 1024;
161
+ if (sizeKb > config.cortexMaxLibSizeKb) {
162
+ return { ok: false, error: `Lib "${filename}" is ${sizeKb.toFixed(1)}KB, max is ${config.cortexMaxLibSizeKb}KB` };
163
+ }
164
+ libs[filename] = content;
165
+ }
166
+ }
167
+ const result = parseCortexManifest(manifestStr, ownerName, Object.keys(libs).length > 0 ? libs : undefined);
168
+ if (!result.ok || !result.extension) {
169
+ return { ok: false, error: result.errors?.join('; ') ?? 'Manifest validation failed' };
170
+ }
171
+ return { ok: true, extension: result.extension, libs: Object.keys(libs).length > 0 ? libs : undefined };
172
+ }
173
+ // ── Internal ZIP extraction ──
174
+ function extractZipEntries(buffer) {
175
+ return new Promise((resolve, reject) => {
176
+ yauzl.fromBuffer(buffer, { lazyEntries: true }, (err, zipfile) => {
177
+ if (err)
178
+ return reject(err);
179
+ const files = new Map();
180
+ let totalDecompressed = 0;
181
+ const maxDecompressed = buffer.length * DECOMPRESSION_RATIO;
182
+ let fileCount = 0;
183
+ zipfile.on('error', reject);
184
+ zipfile.on('entry', (entry) => {
185
+ fileCount++;
186
+ if (fileCount > MAX_FILES) {
187
+ zipfile.close();
188
+ return reject(new Error(`ZIP contains more than ${MAX_FILES} files`));
189
+ }
190
+ const fileName = entry.fileName;
191
+ if (fileName.includes('../') || fileName.startsWith('/')) {
192
+ zipfile.close();
193
+ return reject(new Error(`Dangerous path detected: ${fileName}`));
194
+ }
195
+ // Symlink check (Unix external attributes)
196
+ const unixMode = (entry.externalFileAttributes >> 16) & 0xffff;
197
+ if ((unixMode & 0o170000) === 0o120000) {
198
+ zipfile.close();
199
+ return reject(new Error(`Symlink detected: ${fileName}`));
200
+ }
201
+ const depth = fileName.split('/').length - 1;
202
+ if (depth > MAX_DEPTH) {
203
+ zipfile.close();
204
+ return reject(new Error(`Directory depth exceeds ${MAX_DEPTH}: ${fileName}`));
205
+ }
206
+ if (fileName.endsWith('/')) {
207
+ zipfile.readEntry();
208
+ return;
209
+ }
210
+ zipfile.openReadStream(entry, (readErr, stream) => {
211
+ if (readErr) {
212
+ zipfile.close();
213
+ return reject(readErr);
214
+ }
215
+ const chunks = [];
216
+ stream.on('data', (chunk) => {
217
+ totalDecompressed += chunk.length;
218
+ if (totalDecompressed > maxDecompressed) {
219
+ zipfile.close();
220
+ return reject(new Error('Decompression ratio exceeded (possible zip bomb)'));
221
+ }
222
+ chunks.push(chunk);
223
+ });
224
+ stream.on('end', () => {
225
+ files.set(fileName, Buffer.concat(chunks));
226
+ zipfile.readEntry();
227
+ });
228
+ stream.on('error', (e) => { zipfile.close(); reject(e); });
229
+ });
230
+ });
231
+ zipfile.on('end', () => resolve(files));
232
+ zipfile.readEntry();
233
+ });
234
+ });
235
+ }
236
+ //# sourceMappingURL=upload-zip.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"upload-zip.js","sourceRoot":"","sources":["../../../src/services/upload-zip.ts"],"names":[],"mappings":"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"}
@@ -96,6 +96,38 @@
96
96
  box-shadow: 0 0 0 3px rgba(232, 86, 74, .12);
97
97
  }
98
98
 
99
+ .mode-toggle {
100
+ display: flex;
101
+ gap: 2px;
102
+ flex-shrink: 0;
103
+ background: var(--surface-glass);
104
+ border: 1px solid var(--border-subtle);
105
+ border-radius: var(--radius-sm);
106
+ padding: 2px;
107
+ }
108
+
109
+ .mode-btn {
110
+ padding: 5px 10px;
111
+ border: none;
112
+ border-radius: 6px;
113
+ background: transparent;
114
+ color: var(--text-muted);
115
+ font-size: .78rem;
116
+ cursor: pointer;
117
+ transition: all var(--transition);
118
+ white-space: nowrap;
119
+ }
120
+
121
+ .mode-btn.active {
122
+ background: var(--accent);
123
+ color: #fff;
124
+ font-weight: 600;
125
+ }
126
+
127
+ .mode-btn:hover:not(.active) {
128
+ background: rgba(0,0,0,.05);
129
+ }
130
+
99
131
  .header-actions {
100
132
  display: flex;
101
133
  gap: 8px;
@@ -1002,7 +1034,8 @@
1002
1034
 
1003
1035
  /* ── Published Apps Section ──────────────────────── */
1004
1036
 
1005
- #published-section {
1037
+ #published-section,
1038
+ #community-section {
1006
1039
  margin: 0 24px 24px;
1007
1040
  position: relative;
1008
1041
  z-index: 1;
@@ -1201,6 +1234,10 @@
1201
1234
 
1202
1235
  <header>
1203
1236
  <h1>My Apps</h1>
1237
+ <div class="mode-toggle">
1238
+ <button id="mode-global" class="mode-btn active" onclick="window._launcher.switchDbMode('global')" title="Shared across all accounts on this browser">&#x1F310; Global</button>
1239
+ <button id="mode-personal" class="mode-btn" onclick="window._launcher.switchDbMode('personal')" title="Private to your account">&#x1F464; Personal</button>
1240
+ </div>
1204
1241
  <input type="text" id="search-input" placeholder="Search apps..." autocomplete="off" spellcheck="false"/>
1205
1242
  <div class="header-actions">
1206
1243
  <button class="icon-btn" id="add-btn" title="Add app">+</button>
@@ -1253,7 +1290,7 @@
1253
1290
  </div>
1254
1291
  </div>
1255
1292
 
1256
- <!-- Published Apps Section -->
1293
+ <!-- Published Apps Section (own) -->
1257
1294
  <div id="published-section" style="display:none">
1258
1295
  <div class="published-header" onclick="window._launcher.togglePublished()">
1259
1296
  <h2>&#x1F680; Published Apps <span id="published-count"></span></h2>
@@ -1262,12 +1299,24 @@
1262
1299
  <div id="published-grid" class="published-grid"></div>
1263
1300
  </div>
1264
1301
 
1302
+ <!-- Community Apps Section (other users) -->
1303
+ <div id="community-section" style="display:none">
1304
+ <div class="published-header" onclick="window._launcher.toggleCommunity()">
1305
+ <h2>&#x1F30D; Community Apps <span id="community-count"></span></h2>
1306
+ <span class="arrow open" id="community-arrow">&#x25BC;</span>
1307
+ </div>
1308
+ <div id="community-grid" class="published-grid"></div>
1309
+ </div>
1310
+
1265
1311
  <!-- Recently Opened Strip -->
1266
1312
  <div id="recent-section" style="display:none;margin:0 24px 1rem">
1267
1313
  <h3 style="margin:0 0 .5rem;font-size:.9rem;color:#6B7280">&#x1F552; Recently Opened</h3>
1268
1314
  <div id="recent-strip" style="display:flex;gap:.75rem;overflow-x:auto;padding-bottom:.5rem"></div>
1269
1315
  </div>
1270
1316
 
1317
+ <div id="local-apps-header" style="display:none;margin:0 24px 8px">
1318
+ <h2 style="font-size:1rem;font-weight:700;color:#6366f1;display:flex;align-items:center;gap:8px">&#x1F4BE; Local Apps <span id="local-apps-count" style="opacity:.6;font-weight:400"></span></h2>
1319
+ </div>
1271
1320
  <main id="app-grid"></main>
1272
1321
 
1273
1322
  <footer>
@@ -1509,16 +1558,56 @@
1509
1558
 
1510
1559
  // ── IndexedDB wrapper ──────────────────────────────
1511
1560
 
1512
- const DB_NAME = 'AppLauncherDB';
1561
+ const DB_NAME_GLOBAL = 'AppLauncherDB';
1513
1562
  const DB_VERSION = 2;
1514
1563
  const STORE_NAME = 'apps';
1515
1564
 
1516
1565
  let dbInstance = null;
1566
+ let currentDbMode = localStorage.getItem('appCatalogMode') || 'global';
1567
+
1568
+ function getDbName() {
1569
+ if (currentDbMode === 'personal') {
1570
+ var owner = null;
1571
+ try {
1572
+ if (window.AIMEAT && window.AIMEAT.auth && window.AIMEAT.auth.getSession()) {
1573
+ owner = window.AIMEAT.auth.getSession().owner || null;
1574
+ }
1575
+ if (!owner) {
1576
+ var stored = localStorage.getItem('aimeat_session');
1577
+ if (stored) owner = JSON.parse(stored).owner || null;
1578
+ }
1579
+ } catch(e) {}
1580
+ if (owner) return DB_NAME_GLOBAL + '_' + owner;
1581
+ }
1582
+ return DB_NAME_GLOBAL;
1583
+ }
1584
+
1585
+ function switchDbMode(mode) {
1586
+ if (mode === currentDbMode) return;
1587
+ currentDbMode = mode;
1588
+ localStorage.setItem('appCatalogMode', mode);
1589
+ if (dbInstance) {
1590
+ dbInstance.close();
1591
+ dbInstance = null;
1592
+ }
1593
+ updateModeToggle();
1594
+ refreshAll();
1595
+ }
1596
+
1597
+ function updateModeToggle() {
1598
+ var globalBtn = document.getElementById('mode-global');
1599
+ var personalBtn = document.getElementById('mode-personal');
1600
+ if (!globalBtn || !personalBtn) return;
1601
+ globalBtn.classList.toggle('active', currentDbMode === 'global');
1602
+ personalBtn.classList.toggle('active', currentDbMode === 'personal');
1603
+ }
1517
1604
 
1518
1605
  function openDB() {
1519
- if (dbInstance) return Promise.resolve(dbInstance);
1606
+ var name = getDbName();
1607
+ if (dbInstance && dbInstance.name === name) return Promise.resolve(dbInstance);
1608
+ if (dbInstance) { dbInstance.close(); dbInstance = null; }
1520
1609
  return new Promise(function (resolve, reject) {
1521
- const request = indexedDB.open(DB_NAME, DB_VERSION);
1610
+ const request = indexedDB.open(name, DB_VERSION);
1522
1611
 
1523
1612
  request.onupgradeneeded = function (event) {
1524
1613
  const db = event.target.result;
@@ -2291,6 +2380,12 @@
2291
2380
  }
2292
2381
 
2293
2382
  var grid = document.getElementById('app-grid');
2383
+ var localHeader = document.getElementById('local-apps-header');
2384
+ var localCount = document.getElementById('local-apps-count');
2385
+ if (localHeader) {
2386
+ localHeader.style.display = allApps.length > 0 ? '' : 'none';
2387
+ if (localCount) localCount.textContent = '(' + allApps.length + ')';
2388
+ }
2294
2389
 
2295
2390
  if (filtered.length === 0) {
2296
2391
  if (!activeTag && !searchQuery && allApps.length === 0) {
@@ -3111,14 +3206,25 @@
3111
3206
  arrow.classList.toggle('open', publishedVisible);
3112
3207
  }
3113
3208
 
3209
+ var communityVisible = true;
3210
+ function toggleCommunity() {
3211
+ communityVisible = !communityVisible;
3212
+ var grid = document.getElementById('community-grid');
3213
+ var arrow = document.getElementById('community-arrow');
3214
+ grid.style.display = communityVisible ? '' : 'none';
3215
+ arrow.classList.toggle('open', communityVisible);
3216
+ }
3217
+
3114
3218
  function loadPublishedApps() {
3115
3219
  var config = loadConfig();
3116
3220
  var aimeatUrl = config.aimeatUrl ? config.aimeatUrl.replace(/\/+$/, '') : '';
3117
3221
  var section = document.getElementById('published-section');
3118
3222
  var grid = document.getElementById('published-grid');
3119
3223
  var countEl = document.getElementById('published-count');
3224
+ var communitySection = document.getElementById('community-section');
3225
+ var communityGrid = document.getElementById('community-grid');
3226
+ var communityCountEl = document.getElementById('community-count');
3120
3227
 
3121
- // Get current owner name from session (if logged in)
3122
3228
  var currentOwner = null;
3123
3229
  try {
3124
3230
  if (window.AIMEAT && window.AIMEAT.auth && window.AIMEAT.auth.getSession()) {
@@ -3132,11 +3238,9 @@
3132
3238
  }
3133
3239
  } catch(e) {}
3134
3240
 
3135
- // Always load local published apps
3136
3241
  getAllApps().then(function(apps) {
3137
3242
  var localPublished = apps.filter(function(a) { return a.published; });
3138
3243
 
3139
- // Build a lookup of local published apps by filename
3140
3244
  var localByFilename = {};
3141
3245
  for (var i = 0; i < localPublished.length; i++) {
3142
3246
  var lp = localPublished[i];
@@ -3145,13 +3249,12 @@
3145
3249
  }
3146
3250
  }
3147
3251
 
3148
- // If no aimeatUrl, fall back to local-only display
3149
3252
  if (!aimeatUrl) {
3150
3253
  renderPublishedApps(localPublished, [], localByFilename, aimeatUrl, section, grid, countEl);
3254
+ if (communitySection) communitySection.style.display = 'none';
3151
3255
  return;
3152
3256
  }
3153
3257
 
3154
- // Fetch server apps
3155
3258
  fetch(aimeatUrl + '/v1/apps')
3156
3259
  .then(function(resp) {
3157
3260
  if (!resp.ok) throw new Error('Server returned ' + resp.status);
@@ -3160,20 +3263,61 @@
3160
3263
  .then(function(json) {
3161
3264
  var serverApps = json.data && json.data.apps ? json.data.apps : [];
3162
3265
 
3163
- // Filter to current owner's apps if we know who they are
3164
- if (currentOwner) {
3165
- serverApps = serverApps.filter(function(a) { return a.owner === currentOwner; });
3166
- }
3266
+ var ownApps = currentOwner
3267
+ ? serverApps.filter(function(a) { return a.owner === currentOwner; })
3268
+ : [];
3269
+ var communityApps = currentOwner
3270
+ ? serverApps.filter(function(a) { return a.owner !== currentOwner; })
3271
+ : serverApps;
3167
3272
 
3168
- renderPublishedApps(localPublished, serverApps, localByFilename, aimeatUrl, section, grid, countEl);
3273
+ renderPublishedApps(localPublished, ownApps, localByFilename, aimeatUrl, section, grid, countEl);
3274
+ renderCommunityApps(communityApps, aimeatUrl, communitySection, communityGrid, communityCountEl, currentOwner);
3169
3275
  })
3170
3276
  .catch(function() {
3171
- // Server fetch failed - fall back to local only
3172
3277
  renderPublishedApps(localPublished, [], localByFilename, aimeatUrl, section, grid, countEl);
3278
+ if (communitySection) communitySection.style.display = 'none';
3173
3279
  });
3174
3280
  });
3175
3281
  }
3176
3282
 
3283
+ function renderCommunityApps(serverApps, aimeatUrl, section, grid, countEl, currentOwner) {
3284
+ if (!section || !grid || !countEl) return;
3285
+ if (serverApps.length === 0) {
3286
+ section.style.display = 'none';
3287
+ return;
3288
+ }
3289
+
3290
+ section.style.display = '';
3291
+ countEl.textContent = '(' + serverApps.length + ')';
3292
+
3293
+ var html = '';
3294
+ for (var i = 0; i < serverApps.length; i++) {
3295
+ var sa = serverApps[i];
3296
+ var name = (sa.manifest && sa.manifest.name) ? sa.manifest.name : (sa.filename || '');
3297
+ var version = sa.version_number ? 'v' + sa.version_number : '';
3298
+ var date = sa.created_at ? new Date(sa.created_at).toLocaleDateString() : '';
3299
+ var author = (sa.manifest && sa.manifest.authorDisplay) ? sa.manifest.authorDisplay : (sa.owner || '');
3300
+ var viewUrl = aimeatUrl + '/v1/apps/' + encodeURIComponent(sa.owner || '') + '/' + encodeURIComponent(sa.filename || '');
3301
+ html +=
3302
+ '<div class="published-card">' +
3303
+ '<div class="published-card-name">' + escapeHtml(name) +
3304
+ (version ? ' <span style="opacity:.5;font-size:.85em">' + escapeHtml(version) + '</span>' : '') +
3305
+ '</div>' +
3306
+ '<div class="published-card-meta">' +
3307
+ '<span>' + escapeHtml(sa.filename || '') + '</span>' +
3308
+ '<span>' + date + '</span>' +
3309
+ '</div>' +
3310
+ '<div class="published-card-meta" style="margin-top:2px">' +
3311
+ '<span style="color:var(--accent)">&#x1F464; ' + escapeHtml(author) + '</span>' +
3312
+ '</div>' +
3313
+ '<div class="published-card-actions">' +
3314
+ '<button onclick="window.open(\'' + escapeHtml(viewUrl) + '?mode=inline\', \'_blank\')">&#x1F441; View</button>' +
3315
+ '</div>' +
3316
+ '</div>';
3317
+ }
3318
+ grid.innerHTML = html;
3319
+ }
3320
+
3177
3321
  function renderPublishedApps(localPublished, serverApps, localByFilename, aimeatUrl, section, grid, countEl) {
3178
3322
  // Merge: build a combined list. Server apps take priority for display data,
3179
3323
  // but we note which source(s) each app comes from.
@@ -4040,6 +4184,8 @@
4040
4184
  submitPublish: submitPublish,
4041
4185
  loadPublishedApps: loadPublishedApps,
4042
4186
  togglePublished: togglePublished,
4187
+ toggleCommunity: toggleCommunity,
4188
+ switchDbMode: switchDbMode,
4043
4189
  unpublishApp: unpublishApp,
4044
4190
  deleteServerApp: deleteServerApp,
4045
4191
  renderRecentlyOpened: renderRecentlyOpened,
@@ -4065,11 +4211,19 @@
4065
4211
  window.showCortexPopup = showCortexPopup;
4066
4212
  window.cortexCopy = cortexCopy;
4067
4213
 
4214
+ function refreshAll() {
4215
+ allApps = [];
4216
+ renderApps();
4217
+ loadPublishedApps();
4218
+ renderRecentlyOpened();
4219
+ }
4220
+
4068
4221
  // ── Bootstrap ─────────────────────────────────────
4069
4222
 
4070
4223
  document.addEventListener('DOMContentLoaded', function () {
4071
4224
  // ── Apply theme on load ─────────────────────────
4072
4225
  applyTheme(loadConfig().theme);
4226
+ updateModeToggle();
4073
4227
 
4074
4228
  renderApps();
4075
4229
  loadPublishedApps();
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "aimeat",
3
- "version": "1.3.2",
4
- "description": "AIME AT AI Memory Exchange and Action Transfer reference implementation",
3
+ "version": "1.3.5",
4
+ "description": "AIME AT - AI Memory Exchange and Action Transfer - reference implementation",
5
5
  "type": "module",
6
6
  "main": "dist/src/index.js",
7
7
  "bin": {