aimeat 1.18.0 → 1.19.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.
Files changed (68) hide show
  1. package/README.md +22 -1
  2. package/dist/locales/en.json +28 -0
  3. package/dist/locales/fi.json +28 -0
  4. package/dist/public/css/views/agents-detail.css +145 -0
  5. package/dist/public/css/views/scheduler.css +16 -0
  6. package/dist/public/js/services/admin.js +3 -0
  7. package/dist/public/llms-template.txt +170 -2
  8. package/dist/public/oauth-consent.html +7 -1
  9. package/dist/public/views/admin/portal-tab.js +94 -9
  10. package/dist/public/views/profile/agents/agent-card.js +24 -4
  11. package/dist/public/views/profile/agents/tab-schedules.js +3 -32
  12. package/dist/public/views/profile/agents/tab-tasks.js +5 -2
  13. package/dist/public/views/profile/agents-tab.js +86 -3
  14. package/dist/public/views/profile/agents-tasks-subtab.js +71 -7
  15. package/dist/public/views/profile/schedule-item.js +177 -0
  16. package/dist/public/views/profile/scheduler-tab.js +5 -51
  17. package/dist/src/generated/api-types.d.ts +277 -2
  18. package/dist/src/generated/api-types.d.ts.map +1 -1
  19. package/dist/src/mcp/catalog/scopes.js +1 -1
  20. package/dist/src/mcp/catalog/scopes.js.map +1 -1
  21. package/dist/src/mcp/index.d.ts.map +1 -1
  22. package/dist/src/mcp/index.js +12 -0
  23. package/dist/src/mcp/index.js.map +1 -1
  24. package/dist/src/routes/bootstrap.d.ts +2 -1
  25. package/dist/src/routes/bootstrap.d.ts.map +1 -1
  26. package/dist/src/routes/bootstrap.js +14 -3
  27. package/dist/src/routes/bootstrap.js.map +1 -1
  28. package/dist/src/routes/cortex.d.ts +13 -0
  29. package/dist/src/routes/cortex.d.ts.map +1 -1
  30. package/dist/src/routes/cortex.js +211 -0
  31. package/dist/src/routes/cortex.js.map +1 -1
  32. package/dist/src/routes/extensions.d.ts +14 -0
  33. package/dist/src/routes/extensions.d.ts.map +1 -1
  34. package/dist/src/routes/extensions.js +255 -109
  35. package/dist/src/routes/extensions.js.map +1 -1
  36. package/dist/src/routes/lib-header.d.ts +22 -0
  37. package/dist/src/routes/lib-header.d.ts.map +1 -0
  38. package/dist/src/routes/lib-header.js +244 -0
  39. package/dist/src/routes/lib-header.js.map +1 -0
  40. package/dist/src/routes/libs.d.ts.map +1 -1
  41. package/dist/src/routes/libs.js +13 -0
  42. package/dist/src/routes/libs.js.map +1 -1
  43. package/dist/src/routes/schedules.d.ts.map +1 -1
  44. package/dist/src/routes/schedules.js +19 -5
  45. package/dist/src/routes/schedules.js.map +1 -1
  46. package/dist/src/routes/site.d.ts.map +1 -1
  47. package/dist/src/routes/site.js +39 -1
  48. package/dist/src/routes/site.js.map +1 -1
  49. package/dist/src/server-bootstrap/routes-loader.d.ts.map +1 -1
  50. package/dist/src/server-bootstrap/routes-loader.js +5 -2
  51. package/dist/src/server-bootstrap/routes-loader.js.map +1 -1
  52. package/dist/src/services/prompt-defaults.d.ts.map +1 -1
  53. package/dist/src/services/prompt-defaults.js +35 -0
  54. package/dist/src/services/prompt-defaults.js.map +1 -1
  55. package/dist/src/services/prompt-seeder.d.ts.map +1 -1
  56. package/dist/src/services/prompt-seeder.js +6 -2
  57. package/dist/src/services/prompt-seeder.js.map +1 -1
  58. package/dist/src/services/site.d.ts +4 -0
  59. package/dist/src/services/site.d.ts.map +1 -1
  60. package/dist/src/services/site.js +29 -0
  61. package/dist/src/services/site.js.map +1 -1
  62. package/dist/src/storage/interface.d.ts +1 -1
  63. package/dist/src/storage/interface.d.ts.map +1 -1
  64. package/dist/src/utils/csp-nonce.d.ts +19 -0
  65. package/dist/src/utils/csp-nonce.d.ts.map +1 -0
  66. package/dist/src/utils/csp-nonce.js +25 -0
  67. package/dist/src/utils/csp-nonce.js.map +1 -0
  68. package/package.json +1 -1
package/README.md CHANGED
@@ -15,11 +15,16 @@ AIMEAT is an open protocol for AI agent infrastructure. It gives agents (Claude,
15
15
 
16
16
  <p align="center">
17
17
  <img src="assets/screenshots/portal-landing.png" alt="Portal landing page" width="24%" />
18
- <img src="assets/screenshots/profile-overview.png" alt="User profile" width="24%" />
18
+ <img src="assets/screenshots/profile-overview.png" alt="User profile with the persistent grouped sidebar" width="24%" />
19
19
  <img src="assets/screenshots/admin-dashboard.png" alt="Admin dashboard" width="24%" />
20
20
  <img src="assets/screenshots/app-catalogue-aimeatio.png" alt="App catalogue" width="24%" />
21
21
  </p>
22
22
 
23
+ <p align="center">
24
+ <img src="assets/screenshots/profile-mobile.png" alt="AIMEAT profile on mobile -- compact gold logged-in pill plus the grouped navigation as an off-canvas drawer" width="280" />
25
+ </p>
26
+ <p align="center"><em>The portal and profile are fully responsive: on mobile the grouped navigation collapses into an off-canvas drawer and the logged-in pill stays reachable.</em></p>
27
+
23
28
  ### See it in action (5:50)
24
29
 
25
30
  An AI agent connects to an AIMEAT node and reaches full operational readiness autonomously:
@@ -153,6 +158,12 @@ For MCP-capable runtimes (Claude Desktop, MCP-aware IDEs), run `aimeat connect s
153
158
 
154
159
  Claude Pro, ChatGPT Plus, and other MCP-capable AIs connect directly as MCP clients. OpenClaw, Hermes, Claude Code, and Cursor all work. Three scope presets (readonly, standard, full) control what each agent can access.
155
160
 
161
+ ### Connect agent platforms (Dify, n8n, Open WebUI, ...)
162
+
163
+ AIMEAT also bridges agent platforms. A tool like Dify, n8n, or Open WebUI is its own island -- the agents and data you build there can't reach agents anywhere else. Pointing the platform at an AIMEAT node changes that, and it's a one-time **MCP** connection: add an MCP server for the node's `/v1/mcp` (or a scoped `/v2/mcp/agent` surface), authorize once in the browser, and the full AIMEAT toolset appears -- no token pasting, no per-tool wiring. The agent can even run Hello Integration on itself (paste the canonical instruction from your profile -> Agents), then immediately read/write shared memory, storage, and knowledge packages, discover other agents, and hire capabilities.
164
+
165
+ That's the federation play: each platform is an island, and through AIMEAT its agents move what they build onto the shared network -- where it spreads across federated nodes and other agents can find and use it. Walkthrough: [docs/integrations/dify-hello-integration.md](aimeat/docs/integrations/dify-hello-integration.md).
166
+
156
167
  ### Build apps with AI
157
168
 
158
169
  Tell any AI what you want. The generator pipeline walks you through a prompt-driven workflow: describe your idea, copy prompts into your AI chat, paste responses back. The system validates each component and registers it on your node. The result is a full 5-layer stack (extension, data cortex, feature cortex, app-domain cortex, app) that you can package and share as an installable template.
@@ -299,6 +310,12 @@ With `AIMEAT_ANONYMOUS=true`, anyone can read and do limited writes without regi
299
310
 
300
311
  Each node runs independently with its own identity and portal. Operators customize through CSS themes (`theme.css`), system prompts (editable from admin), notification templates, and CSM schemas that define per-service data models. Run your own node, your own branding.
301
312
 
313
+ ### Custom portal templates
314
+
315
+ The public landing page (`/`) is editable live from the admin **Portal** tab — write the HTML template (with serve-time `{{config:*}}`, `{{memory:portal/*}}`, `{{kv:*}}`, `{{board:*}}` tags), manage portal memory keys and KV pairs, and watch the result render in the inline preview. Following AIMEAT's prompt-driven workflow, the **AI-Assisted Editor** hands you a ready prompt: paste it into any AI chat, paste the JSON bundle it returns back into **Import AI Result**, and your node's front page immediately looks custom. Drop in `<script src="/v1/libs/aimeat-header.js"></script>` and the page also carries the **exact same site header** as the rest of AIMEAT — brand, navigation, theme and language switchers, and the live gold login pill — so visitors can always sign in and reach their profile. Operator templates are trusted: their inline `<script>` runs under the node's CSP via a per-request nonce.
316
+
317
+ <img src="assets/screenshots/portal-admin-editor.png" alt="Admin Portal tab: live preview of the custom landing page, the HTML template editor with tag reference and active-source status, portal memory keys, KV pairs, and the AI-Assisted Editor (Load AI Prompt -> Import AI Result)" width="760" />
318
+
302
319
  ---
303
320
 
304
321
  ## Getting Started
@@ -375,6 +392,10 @@ cd aimeat && pnpm exec node --env-file=.env.test.sqlite --import tsx test/run-e2
375
392
 
376
393
  > The legacy `pnpm test:e2e` (in-memory backend) is deprecated and produces stale failures. Use the SQLite or MongoDB commands instead.
377
394
 
395
+ ### Synthetic agent traces (dev tool)
396
+
397
+ [`aimeat/tools/synthtraces/`](aimeat/tools/synthtraces/) is a small self-play harness that generates synthetic AIMEAT agent-session traces: a *persona* model plays the human owner while an *agent* model drives a real node over REST or MCP, producing task-driven traces you can use to benchmark or fine-tune models on the protocol. It runs on free cloud models (OpenRouter `owl-alpha`) or **fully local** via Ollama (`qwen2.5` + `llama3.2`), and ships an eval that scores protocol-correctness (no hallucinated tools, valid memory keys, task completion, token cost). See [tools/README.md](aimeat/tools/README.md).
398
+
378
399
  ---
379
400
 
380
401
  ## Documentation
@@ -80,6 +80,13 @@
80
80
  "profile.scheduler.budgetDesc": "Default opt-in limits applied to schedules created for this agent.",
81
81
  "profile.scheduler.budgetSave": "Save budget guards",
82
82
  "profile.scheduler.budgetSaved": "Budget guards saved",
83
+ "profile.scheduler.edit": "Edit",
84
+ "profile.scheduler.save": "Save",
85
+ "profile.scheduler.saved": "Schedule updated",
86
+ "profile.scheduler.dispatches": "Creates each run",
87
+ "profile.scheduler.soon": "soon",
88
+ "profile.scheduler.paused": "paused",
89
+ "profile.scheduler.autoKey": "(auto)",
83
90
  "common": {
84
91
  "cancel": "Cancel",
85
92
  "confirm": "Confirm",
@@ -347,6 +354,13 @@
347
354
  },
348
355
  "agents": {
349
356
  "title": "Your Agents",
357
+ "active": {
358
+ "title": "Running now",
359
+ "empty": "No tasks running right now.",
360
+ "openHint": "Open this task",
361
+ "untitled": "Untitled task",
362
+ "more": "more running (open an agent to see all)"
363
+ },
350
364
  "desc": "Agents are AI identities registered under your account, each with their own identity, scopes, tools, tasks, and memory access. Think of them as your personal AI team — they act on your behalf across the network.",
351
365
  "connect": "Connect an Automation Agent",
352
366
  "connectDesc": "Connect an AI agent to your account using the AIMEAT connector CLI or an agent-capable runtime. The connector is the easiest path because it gives the agent a persistent identity, approved scopes, local skill bundle, and MCP access to AIMEAT tools:",
@@ -3676,6 +3690,20 @@
3676
3690
  "portalLbStatus": "Sync Status",
3677
3691
  "portalLbSyncNow": "Sync Now",
3678
3692
  "portalLbSyncDone": "Portal sync completed.",
3693
+ "portalActiveSource": "Active source",
3694
+ "portalCustomActive": "Custom template",
3695
+ "portalDefaultActive": "Default app (built-in SPA)",
3696
+ "portalDefaultExplain": "No custom template is stored, so visitors see the built-in app. The editor below starts empty. Click \"{action}\" to load the current page's HTML as a starting point, then edit and save it to override the default.",
3697
+ "portalCustomExplain": "A custom template is active and overrides the built-in app. Edit it below and save, or reset to default to bring the built-in app back.",
3698
+ "portalLoadCurrent": "Load current page",
3699
+ "portalLoadCurrentConfirm": "Replace the editor contents with the current page's HTML? Unsaved edits will be lost.",
3700
+ "portalLoadedCurrent": "Loaded current page into the editor — review and click Save Template to apply.",
3701
+ "portalAiBundleExplain": "Paste the JSON bundle the AI produced here. It can include the template, portal memory keys, and KV pairs — they are all applied at once.",
3702
+ "portalAiBundlePlaceholder": "Paste the AI's JSON result here: { \"template\": \"<!DOCTYPE html>...\", \"memory\": { \"portal/about\": \"...\" }, \"kv\": {} }",
3703
+ "portalImportDone": "Imported: {summary}",
3704
+ "portalImportInvalid": "That doesn't look like a valid AI bundle. Expected JSON with a \"template\", \"memory\", or \"kv\" field.",
3705
+ "portalSaveLooksLikeJson": "This looks like an AI JSON bundle, not an HTML template. Paste it into \"Import AI Result\" below instead.",
3706
+ "portalMemorySaved": "Portal memory key saved.",
3679
3707
  "cors": "CORS",
3680
3708
  "corsTitle": "CORS Origin Management",
3681
3709
  "corsNodeDefault": "Node Default Origins",
@@ -80,6 +80,13 @@
80
80
  "profile.scheduler.budgetDesc": "Oletusarvoiset valinnaiset rajat, jotka koskevat tälle agentille luotuja ajastuksia.",
81
81
  "profile.scheduler.budgetSave": "Tallenna budjettirajat",
82
82
  "profile.scheduler.budgetSaved": "Budjettirajat tallennettu",
83
+ "profile.scheduler.edit": "Muokkaa",
84
+ "profile.scheduler.save": "Tallenna",
85
+ "profile.scheduler.saved": "Ajastus päivitetty",
86
+ "profile.scheduler.dispatches": "Luo joka ajolla",
87
+ "profile.scheduler.soon": "pian",
88
+ "profile.scheduler.paused": "tauolla",
89
+ "profile.scheduler.autoKey": "(auto)",
83
90
  "common": {
84
91
  "cancel": "Peruuta",
85
92
  "confirm": "Vahvista",
@@ -347,6 +354,13 @@
347
354
  },
348
355
  "agents": {
349
356
  "title": "Agenttisi",
357
+ "active": {
358
+ "title": "Käynnissä nyt",
359
+ "empty": "Ei käynnissä olevia taskeja juuri nyt.",
360
+ "openHint": "Avaa tämä taski",
361
+ "untitled": "Nimetön taski",
362
+ "more": "lisää käynnissä (avaa agentti nähdäksesi kaikki)"
363
+ },
350
364
  "desc": "Agentit ovat tilillesi rekisteröityjä AI-identiteettejä, joilla on oma identiteetti, rajaukset, työkalut, tehtävät ja muistipääsy. Ajattele heitä henkilökohtaisena AI-tiiminäsi - ne toimivat puolestasi verkossa.",
351
365
  "connect": "Yhdistä automaatioagentti",
352
366
  "connectDesc": "Yhdistä tekoälyagentti tilillesi AIMEAT-liittimellä tai agenttia ajavassa ympäristössä. Liitin on helpoin polku, koska se antaa agentille pysyvän identiteetin, hyväksytyt rajaukset, paikallisen taitopaketin ja MCP-pääsyn AIMEAT-työkaluihin:",
@@ -3676,6 +3690,20 @@
3676
3690
  "portalLbStatus": "Synkronointitila",
3677
3691
  "portalLbSyncNow": "Synkronoi nyt",
3678
3692
  "portalLbSyncDone": "Portaalin synkronointi valmis.",
3693
+ "portalActiveSource": "Aktiivinen lähde",
3694
+ "portalCustomActive": "Mukautettu mallipohja",
3695
+ "portalDefaultActive": "Oletussovellus (sisäänrakennettu SPA)",
3696
+ "portalDefaultExplain": "Mukautettua mallipohjaa ei ole tallennettu, joten kävijät näkevät sisäänrakennetun sovelluksen. Alla oleva editori on tyhjä. Paina \"{action}\" ladataksesi nykyisen sivun HTML:n lähtökohdaksi, muokkaa sitä ja tallenna korvataksesi oletuksen.",
3697
+ "portalCustomExplain": "Mukautettu mallipohja on käytössä ja korvaa sisäänrakennetun sovelluksen. Muokkaa sitä alla ja tallenna, tai palauta oletukseksi tuodaksesi sisäänrakennetun sovelluksen takaisin.",
3698
+ "portalLoadCurrent": "Lataa nykyinen sivu pohjaksi",
3699
+ "portalLoadCurrentConfirm": "Korvataanko editorin sisältö nykyisen sivun HTML:llä? Tallentamattomat muutokset menetetään.",
3700
+ "portalLoadedCurrent": "Nykyinen sivu ladattu editoriin — tarkista ja paina Tallenna mallipohja ottaaksesi käyttöön.",
3701
+ "portalAiBundleExplain": "Liitä tähän tekoälyn tuottama JSON-paketti. Se voi sisältää mallipohjan, portaalin muistiavaimet ja KV-parit — ne otetaan käyttöön kaikki kerralla.",
3702
+ "portalAiBundlePlaceholder": "Liitä tekoälyn JSON-tulos tähän: { \"template\": \"<!DOCTYPE html>...\", \"memory\": { \"portal/about\": \"...\" }, \"kv\": {} }",
3703
+ "portalImportDone": "Tuotu: {summary}",
3704
+ "portalImportInvalid": "Tämä ei näytä kelvolliselta tekoälypaketilta. Odotettiin JSON, jossa on \"template\", \"memory\" tai \"kv\" -kenttä.",
3705
+ "portalSaveLooksLikeJson": "Tämä näyttää tekoälyn JSON-paketilta, ei HTML-mallipohjalta. Liitä se alla olevaan \"Tuo tekoälyn tulos\" -kenttään.",
3706
+ "portalMemorySaved": "Portaalin muistiavain tallennettu.",
3679
3707
  "cors": "CORS",
3680
3708
  "corsTitle": "CORS-alkuperähallinta",
3681
3709
  "corsNodeDefault": "Solmun oletusalkuperät",
@@ -4,6 +4,10 @@
4
4
  * Uses agd- prefix to avoid collisions with other profile styles.
5
5
  * All colors use CSS variables from theme.css.
6
6
  * @version-history
7
+ * v1.10.0 -- 2026-06-03 -- Add fleet "running now" panel styles (.pf-agd-active,
8
+ * -head, -empty, -list, -row, -dot, -agent, -title, -time) for the cross-agent
9
+ * active-tasks list between the board and the agent list. All colors tokenized
10
+ * (var(--success) running dot/hover, var(--card-bg)/-alt, --border, --text-dim/-muted).
7
11
  * v1.9.0 -- 2026-06-02 -- Component unification (#24 search bar): tokenize the
8
12
  * hardcoded hex fallbacks in .pf-agd-search-input (var(--border, #E5E7EB) ->
9
13
  * var(--border); var(--card-bg, #FFFFFF) -> var(--card); var(--text, #1A1A2E)
@@ -49,6 +53,7 @@
49
53
  * v2.22.0 -- 2026-06-02 -- Add divider line above the tab bar (border-top + spacing) so tabs don't blend with the status row above
50
54
  * v2.23.0 -- 2026-06-02 -- Compact .md-body styles for agent message bubbles (Markdown-rendered outbound replies)
51
55
  * v2.24.0 -- 2026-06-02 -- Compact .md-body styles for Markdown-rendered non-JSON task memory values (pf-agd-task-memory-md)
56
+ * v2.25.0 -- 2026-06-03 -- Compact .md-body styles for Markdown-rendered task descriptions (pf-agd-task-desc) so long agent-authored prompts render with structure
52
57
  * v2.11.0 -- 2026-05-31 -- Widen the solo pop-out window (and pf-agd-solo max-width) so the agent card matches the list-view width and the full tab bar fits
53
58
  * v2.12.0 -- 2026-05-31 -- Add completed-task deliverable styles (pf-agd-deliverable*) linking a task to its agent-memory output key
54
59
  * v2.13.0 -- 2026-05-31 -- Add per-task blur-title toggle styles (pf-agd-blur-toggle, pf-agd-task-title--blurred) for hiding sensitive titles in screen recordings
@@ -1231,6 +1236,76 @@
1231
1236
  padding: 1rem;
1232
1237
  margin-bottom: 1rem;
1233
1238
  }
1239
+
1240
+ /* Fleet "running now" panel (between the board and the agent list). */
1241
+ .pf .pf-agd-active {
1242
+ background: var(--card-bg, #FFFFFF);
1243
+ border: 1px solid var(--border, #E5E7EB);
1244
+ border-radius: var(--radius, 8px);
1245
+ padding: 0.75rem 1rem;
1246
+ margin-bottom: 1rem;
1247
+ }
1248
+ .pf .pf-agd-active-head {
1249
+ margin-bottom: 0.5rem;
1250
+ }
1251
+ .pf .pf-agd-active-empty {
1252
+ font-size: 0.8rem;
1253
+ color: var(--text-dim, #6B7280);
1254
+ }
1255
+ .pf .pf-agd-active-list {
1256
+ display: flex;
1257
+ flex-direction: column;
1258
+ gap: 0.25rem;
1259
+ }
1260
+ .pf .pf-agd-active-row {
1261
+ display: flex;
1262
+ align-items: center;
1263
+ gap: 0.5rem;
1264
+ width: 100%;
1265
+ text-align: left;
1266
+ background: var(--card-bg-alt, #F3F4F6);
1267
+ border: 1px solid var(--border, #E5E7EB);
1268
+ border-radius: var(--radius-xs, 6px);
1269
+ padding: 0.4rem 0.6rem;
1270
+ font: inherit;
1271
+ color: inherit;
1272
+ cursor: pointer;
1273
+ transition: border-color 0.15s, background 0.15s;
1274
+ }
1275
+ .pf .pf-agd-active-row:hover {
1276
+ background: var(--card-bg, #FFFFFF);
1277
+ border-color: var(--success, #10B981);
1278
+ }
1279
+ .pf .pf-agd-active-dot {
1280
+ flex: 0 0 auto;
1281
+ width: 8px;
1282
+ height: 8px;
1283
+ border-radius: 50%;
1284
+ background: var(--success, #10B981);
1285
+ }
1286
+ .pf .pf-agd-active-agent {
1287
+ flex: 0 0 auto;
1288
+ max-width: 30%;
1289
+ font-weight: 600;
1290
+ font-size: 0.8rem;
1291
+ white-space: nowrap;
1292
+ overflow: hidden;
1293
+ text-overflow: ellipsis;
1294
+ }
1295
+ .pf .pf-agd-active-title {
1296
+ flex: 1 1 auto;
1297
+ min-width: 0;
1298
+ font-size: 0.8rem;
1299
+ color: var(--text-dim, #6B7280);
1300
+ white-space: nowrap;
1301
+ overflow: hidden;
1302
+ text-overflow: ellipsis;
1303
+ }
1304
+ .pf .pf-agd-active-time {
1305
+ flex: 0 0 auto;
1306
+ font-size: 0.72rem;
1307
+ color: var(--text-muted, #9CA3AF);
1308
+ }
1234
1309
  .pf .pf-agd-board-grid {
1235
1310
  display: grid;
1236
1311
  grid-template-columns: repeat(3, 1fr);
@@ -3295,6 +3370,76 @@
3295
3370
  }
3296
3371
  .pf .pf-agd-task-memory-md .md-body th { background: var(--card-bg, #FFFFFF); }
3297
3372
 
3373
+ /* Markdown-rendered task descriptions (agent-authored prompts are often long and
3374
+ structured). Compact, mirroring the task-memory variant. The expanded card
3375
+ background is grey (--card-bg-alt), so code chips get a white fill + border for
3376
+ contrast. Paragraphs keep the dim base colour; headings use the normal text
3377
+ colour to stand out. */
3378
+ .pf .pf-agd-task-desc .md-body > :first-child { margin-top: 0; }
3379
+ .pf .pf-agd-task-desc .md-body > :last-child { margin-bottom: 0; }
3380
+ .pf .pf-agd-task-desc .md-body p { margin: 0.3rem 0; }
3381
+ .pf .pf-agd-task-desc .md-body h1,
3382
+ .pf .pf-agd-task-desc .md-body h2,
3383
+ .pf .pf-agd-task-desc .md-body h3,
3384
+ .pf .pf-agd-task-desc .md-body h4 {
3385
+ margin: 0.5rem 0 0.2rem;
3386
+ line-height: 1.3;
3387
+ font-size: 0.85rem;
3388
+ color: var(--text, #1A1A2E);
3389
+ }
3390
+ .pf .pf-agd-task-desc .md-body h1 { font-size: 0.95rem; }
3391
+ .pf .pf-agd-task-desc .md-body h2 { font-size: 0.9rem; }
3392
+ .pf .pf-agd-task-desc .md-body ul,
3393
+ .pf .pf-agd-task-desc .md-body ol { margin: 0.3rem 0; padding-left: 1.3rem; }
3394
+ .pf .pf-agd-task-desc .md-body li { margin: 0.12rem 0; }
3395
+ .pf .pf-agd-task-desc .md-body a { color: var(--accent, #E8564A); }
3396
+ .pf .pf-agd-task-desc .md-body strong { color: var(--text, #1A1A2E); }
3397
+ .pf .pf-agd-task-desc .md-body blockquote {
3398
+ margin: 0.35rem 0;
3399
+ padding: 0.15rem 0.55rem;
3400
+ border-left: 3px solid var(--border, #E5E7EB);
3401
+ color: var(--text-dim, #6B7280);
3402
+ }
3403
+ .pf .pf-agd-task-desc .md-body .md-code {
3404
+ font-family: var(--font-mono, ui-monospace, Menlo, Consolas, monospace);
3405
+ font-size: 0.72rem;
3406
+ background: var(--card-bg, #FFFFFF);
3407
+ border: 1px solid var(--border, #E5E7EB);
3408
+ padding: 0.02rem 0.3rem;
3409
+ border-radius: 4px;
3410
+ }
3411
+ .pf .pf-agd-task-desc .md-body .md-pre {
3412
+ margin: 0.4rem 0;
3413
+ padding: 0.55rem;
3414
+ background: var(--card-bg, #FFFFFF);
3415
+ border: 1px solid var(--border, #E5E7EB);
3416
+ border-radius: 6px;
3417
+ overflow-x: auto;
3418
+ }
3419
+ .pf .pf-agd-task-desc .md-body .md-pre .md-code {
3420
+ display: block;
3421
+ white-space: pre;
3422
+ background: none;
3423
+ border: none;
3424
+ padding: 0;
3425
+ tab-size: 4;
3426
+ font-size: 0.72rem;
3427
+ line-height: 1.35;
3428
+ color: var(--text, #1A1A2E);
3429
+ }
3430
+ .pf .pf-agd-task-desc .md-body table {
3431
+ border-collapse: collapse;
3432
+ margin: 0.35rem 0;
3433
+ font-size: 0.72rem;
3434
+ }
3435
+ .pf .pf-agd-task-desc .md-body th,
3436
+ .pf .pf-agd-task-desc .md-body td {
3437
+ border: 1px solid var(--border, #E5E7EB);
3438
+ padding: 0.2rem 0.45rem;
3439
+ text-align: left;
3440
+ }
3441
+ .pf .pf-agd-task-desc .md-body th { background: var(--card-bg, #FFFFFF); }
3442
+
3298
3443
  /* Structured JSON view: key/value rows, nested blocks indented with a guide line. */
3299
3444
  .pf .pf-agd-json-block { display: flex; flex-direction: column; gap: 0.05rem; }
3300
3445
  .pf .pf-agd-json-row {
@@ -98,3 +98,19 @@
98
98
  .sch-form-actions { display: flex; justify-content: flex-end; }
99
99
 
100
100
  .sch-budget-section { margin-bottom: 1rem; }
101
+
102
+ /* ── Editable schedule card (master + agent views) ── */
103
+ .sch-card-list { display: flex; flex-direction: column; gap: 0.6rem; }
104
+ .sch-card {
105
+ background: var(--card); border: 1px solid var(--border); border-radius: var(--radius);
106
+ padding: 0.75rem 0.9rem; display: flex; flex-direction: column; gap: 0.5rem;
107
+ }
108
+ .sch-card-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 0.75rem; flex-wrap: wrap; }
109
+ .sch-card-title { display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; min-width: 0; }
110
+ .sch-card-meta { display: flex; flex-wrap: wrap; gap: 0.3rem 1rem; font-size: 0.82rem; color: var(--text); }
111
+ .sch-meta-k { color: var(--text-dim); }
112
+ .sch-card-dispatch { background: var(--bg-dim); border-radius: var(--radius-sm); padding: 0.5rem 0.7rem; }
113
+ .sch-dispatch-label { font-size: 0.66rem; font-weight: 700; letter-spacing: 0.05em; text-transform: uppercase; color: var(--text-dim); margin-bottom: 0.2rem; }
114
+ .sch-dispatch-title { font-weight: 600; font-size: 0.9rem; color: var(--text); white-space: pre-wrap; }
115
+ .sch-dispatch-body { font-size: 0.84rem; color: var(--text-dim); margin-top: 0.15rem; white-space: pre-wrap; word-break: break-word; }
116
+ .sch-edit { display: flex; flex-direction: column; gap: 0.55rem; border-top: 1px solid var(--border); padding-top: 0.6rem; }
@@ -152,6 +152,9 @@ export const saveSiteTemplate = (tmpl) => apiPost('/v1/site/template', { templa
152
152
  export const deleteSiteTemplate = () => apiDelete('/v1/site/template');
153
153
  export const getSiteChangelog = () => apiGet('/v1/site/changelog');
154
154
  export const getSiteMemoryKeys = () => apiGet('/v1/site/memory-keys');
155
+ export const setSiteMemory = (key, value) => apiPost('/v1/site/memory', { key, value });
156
+ export const deleteSiteMemory = (key) => apiDelete(`/v1/site/memory/${encodeURIComponent(key)}`);
157
+ export const importSiteBundle = (bundle) => apiPost('/v1/site/import', bundle);
155
158
  export const clearSiteCache = () => apiPost('/v1/site/cache-invalidate');
156
159
  export const getSitePrompt = () => apiGet('/v1/site/prompt');
157
160
  export const triggerLbSync = () => apiPost('/v1/admin/site/sync');
@@ -35,6 +35,16 @@ write the HTML using the starter template and SDK libraries below.
35
35
  Do not ask about deployment, cortex, extensions, CSM, or
36
36
  architecture. Just ask what the app should do and start building.
37
37
 
38
+ Two starter templates follow. Pick by who needs to read the content:
39
+
40
+ - **Starter Template** (default, login-gated) — the visitor signs in and
41
+ works with their own data (notes, trackers, multiplayer, galleries).
42
+ Nothing renders until there is a session.
43
+ - **Public viewer template** (readable without login) — the content is
44
+ shown to everyone, no account required; the owner logs in only to edit.
45
+ Use it for public-facing readers: a public newspaper, directory,
46
+ noticeboard, or gallery that pulls from PUBLIC memory.
47
+
38
48
  ### Starter Template (use this for every app)
39
49
 
40
50
  ```html
@@ -93,6 +103,164 @@ entry: index.html
93
103
  </html>
94
104
  ```
95
105
 
106
+ ### Public viewer template — readable without login
107
+
108
+ Use this when the content must be visible to anyone, with no account
109
+ (public newspaper, directory, noticeboard, gallery). It differs from the
110
+ default Starter Template in three ways:
111
+
112
+ 1. **`startApp()` runs for everyone.** The login bar still mounts, but the
113
+ app never waits for a session — anonymous visitors render immediately.
114
+ 2. **Reads use `getPublic(gaii, key)` only.** This is the single read that
115
+ works without a token (it hits `GET /v1/memory/:gaii/:key` and returns
116
+ PUBLIC entries). Do NOT use `AIMEAT.data.get/list/search` for the shown
117
+ content — those require a session and read the *caller's* namespace, not
118
+ the publisher's. There is no anonymous "list public keys" call.
119
+ 3. **Content lives behind one public index key.** The publisher keeps a
120
+ single PUBLIC key (a "front page") whose value lists each item with its
121
+ own `gaii` + `key`. The viewer reads the index, then fans out to each
122
+ item. The bodies can sit under many different authors (e.g. several
123
+ writer agents) — only the index has a fixed home, and it carries every
124
+ item's full `gaii`, so the app never has to know each author up front.
125
+
126
+ ```html
127
+ <!-- AIMEAT App Manifest
128
+ name: public-viewer
129
+ version: 1.0.0
130
+ description: Reads public memory and shows it to everyone — no login required
131
+ entry: index.html
132
+ -->
133
+ <!DOCTYPE html>
134
+ <html lang="en">
135
+ <head>
136
+ <meta charset="UTF-8">
137
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
138
+ <title>Public Viewer</title>
139
+ <link href="https://cdn.jsdelivr.net/npm/daisyui@5" rel="stylesheet" />
140
+ <script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
141
+ </head>
142
+ <body class="bg-base-100 min-h-screen flex flex-col">
143
+ <nav class="navbar bg-base-200 shadow-sm px-4">
144
+ <div class="flex-1"><span class="text-lg font-bold">Public Viewer</span></div>
145
+ <!-- Login bar mounts for everyone. Anonymous visitors just read; the
146
+ owner can sign in to edit. Reading never depends on it. -->
147
+ <div class="flex-none"><span id="header-auth"></span></div>
148
+ </nav>
149
+ <div id="app" class="flex-1 p-4">Loading…</div>
150
+ <script>
151
+ // ── Where the public index lives (fill these in) ──
152
+ // PUBLISHER is the gaii that owns INDEX_KEY:
153
+ // • owner-run public site → the owner GHII, e.g. 'alice@aimeat-fi-001-genesis'
154
+ // • agent/pipeline-fed feed → the publishing agent GAII,
155
+ // e.g. 'editor#alice@aimeat-fi-001-genesis'
156
+ // (To auto-derive the owner GHII from the app's /v1/apps/:owner/ URL, read
157
+ // it from location.pathname — but an explicit value is clearer and avoids
158
+ // guessing the node id.)
159
+ const PUBLISHER = 'OWNER_OR_AGENT@NODE_ID'; // ← set this
160
+ const INDEX_KEY = 'newspaper.frontpage'; // ← a PUBLIC key holding the item list
161
+
162
+ function loadScript(src) {
163
+ return new Promise((resolve, reject) => {
164
+ const s = document.createElement('script');
165
+ s.src = src; s.onload = resolve; s.onerror = reject;
166
+ document.head.appendChild(s);
167
+ });
168
+ }
169
+
170
+ async function boot() {
171
+ await loadScript('/v1/libs/aimeat-auth.js');
172
+ await loadScript('/v1/libs/aimeat-data.js');
173
+
174
+ // The login bar renders for everyone. Owner login unlocks editing;
175
+ // logout returns to read-only. Re-render on either so the UI matches.
176
+ AIMEAT.auth.mountLoginButton('#header-auth', {
177
+ onLogin: () => location.reload(),
178
+ onLogout: () => location.reload(),
179
+ });
180
+
181
+ // Restore an existing session if there is one — but DO NOT gate on it.
182
+ await AIMEAT.auth.login(); // owner session, or null for an anonymous visitor
183
+ startApp(); // ALWAYS render — anonymous visitors included
184
+ }
185
+
186
+ async function startApp() {
187
+ const session = AIMEAT.auth.getSession(); // null when nobody is logged in
188
+ // Editing is offered only when the logged-in owner actually owns the index.
189
+ // For a pipeline-fed feed (PUBLISHER is an agent), the app stays read-only
190
+ // and the pipeline maintains the front page.
191
+ const canEdit = !!session && session.ghii === PUBLISHER;
192
+ const app = document.getElementById('app');
193
+
194
+ // ── Anonymous-safe read: the public front-page index ──
195
+ const index = await AIMEAT.data.getPublic(PUBLISHER, INDEX_KEY) || [];
196
+ // index item shape (you choose it): { gaii, key, title, date, summary }
197
+
198
+ // Fan out to each item's public body. Items may live under different
199
+ // authors — the index carries each item's full gaii.
200
+ const items = (await Promise.all(index.map(async (item) => {
201
+ const body = await AIMEAT.data.getPublic(item.gaii, item.key);
202
+ return body ? { ...item, body } : null;
203
+ }))).filter(Boolean);
204
+
205
+ render(app, items, canEdit);
206
+ }
207
+
208
+ function render(app, items, canEdit) {
209
+ const header = canEdit
210
+ ? '<div class="mb-4"><button id="new-btn" class="btn btn-primary btn-sm">New article</button></div>'
211
+ : '';
212
+ const body = items.length
213
+ ? items.map(a =>
214
+ '<article class="card bg-base-200 mb-3"><div class="card-body">' +
215
+ '<h2 class="card-title">' + esc(a.title || a.key) + '</h2>' +
216
+ '<p class="text-xs opacity-60">' + esc(a.date || '') + '</p>' +
217
+ // Public content can come from anyone — ALWAYS escape before insert.
218
+ // Never assign a raw public value to innerHTML.
219
+ '<div>' + esc(typeof a.body === 'string' ? a.body : (a.body.text ?? JSON.stringify(a.body))) + '</div>' +
220
+ '</div></article>'
221
+ ).join('')
222
+ : '<p class="opacity-70">No content published yet.</p>';
223
+ app.innerHTML = header + body;
224
+ if (canEdit) document.getElementById('new-btn').onclick = publishArticle;
225
+ }
226
+
227
+ // Owner-only: write a PUBLIC article and prepend it to the PUBLIC index.
228
+ // Both writes land in the owner's GHII namespace (= PUBLISHER here), so the
229
+ // viewer reads them straight back via getPublic.
230
+ async function publishArticle() {
231
+ const title = prompt('Title?'); if (!title) return;
232
+ const text = prompt('Body?') || '';
233
+ const session = AIMEAT.auth.getSession();
234
+ const now = new Date().toISOString();
235
+ const key = 'newspaper.article.' + Date.now();
236
+ await AIMEAT.data.set(key, { title, text, date: now }, { visibility: 'public' });
237
+ const index = await AIMEAT.data.getPublic(PUBLISHER, INDEX_KEY) || [];
238
+ index.unshift({ gaii: session.ghii, key, title, date: now });
239
+ await AIMEAT.data.set(INDEX_KEY, index, { visibility: 'public' });
240
+ location.reload();
241
+ }
242
+
243
+ function esc(s) { const d = document.createElement('div'); d.textContent = s == null ? '' : String(s); return d.innerHTML; }
244
+ boot();
245
+ </script>
246
+ </body>
247
+ </html>
248
+ ```
249
+
250
+ **Public viewer rules:**
251
+ - Call `startApp()` unconditionally. Never `if (session) startApp()` — that is
252
+ what leaves anonymous visitors stuck on "Loading…".
253
+ - `getPublic(gaii, key)` is the only anonymous read. `get/list/search/set`
254
+ all require a login and operate on the caller's own namespace.
255
+ - Everything the public sees must be written with `visibility: 'public'` —
256
+ the index key and every item body.
257
+ - The index is the single source the app must know; it carries each item's
258
+ full `gaii`, so bodies can be spread across many author agents.
259
+ - Public content is untrusted input. Escape it before inserting into the DOM
260
+ (the template's `esc()` does this). Never `innerHTML` a raw public value.
261
+ - Owner editing is a UX affordance gated on `session.ghii === PUBLISHER`; the
262
+ server is the real boundary (it rejects public writes without an owner session).
263
+
96
264
  ### SDK Libraries (add to boot() as needed)
97
265
 
98
266
  | Library | Load with | Use for |
@@ -340,7 +508,7 @@ entry: index.html
340
508
  // AIMEAT.data.delete(key) — delete entry
341
509
  // AIMEAT.data.search(query) — search across keys and values
342
510
  // AIMEAT.data.list() — list all keys
343
- // AIMEAT.data.getPublic(gaii, key) — read another user's public data
511
+ // AIMEAT.data.getPublic(gaii, key) — read another user's public data (no login required)
344
512
 
345
513
  // Example: load saved data or initialize
346
514
  let notes = await AIMEAT.data.get('my-app.notes') || [];
@@ -576,7 +744,7 @@ await AIMEAT.data.update(key, value, version) // optimistic locking update
576
744
  await AIMEAT.data.delete(key) // delete entry
577
745
  await AIMEAT.data.list() // list all keys
578
746
  await AIMEAT.data.search(query) // full-text search
579
- await AIMEAT.data.getPublic(gaii, key) // read another user's public data
747
+ await AIMEAT.data.getPublic(gaii, key) // read another user's public data (no login — the only anonymous read; see "Public viewer template")
580
748
  ```
581
749
 
582
750
  **AIMEAT.storage** (`/v1/libs/aimeat-storage.js`):
@@ -363,7 +363,13 @@
363
363
  });
364
364
  }
365
365
  } else {
366
- throw new Error(d.error_description || d.error || t('errorGeneric'));
366
+ // d.error may be an OAuth string ("invalid_request") OR an AIMEAT
367
+ // envelope object ({code,message}); never stringify the object as
368
+ // "[object Object]" — surface the real message.
369
+ const msg = d.error_description
370
+ || (typeof d.error === 'string' ? d.error : d.error && d.error.message)
371
+ || t('errorGeneric');
372
+ throw new Error(msg);
367
373
  }
368
374
  } catch (err) {
369
375
  errEl.textContent = err.message;