aimeat 1.26.0 → 1.27.1

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 (307) hide show
  1. package/README.md +7 -0
  2. package/dist/.env.example +19 -0
  3. package/dist/locales/en.json +507 -288
  4. package/dist/locales/fi.json +476 -257
  5. package/dist/public/css/components/app-sandbox.css +62 -0
  6. package/dist/public/css/views/app-grant.css +83 -0
  7. package/dist/public/css/views/inbox.css +30 -0
  8. package/dist/public/css/views/landing.css +16 -2
  9. package/dist/public/css/views/living.css +187 -0
  10. package/dist/public/css/views/notebook.css +89 -0
  11. package/dist/public/css/views/profile.css +42 -85
  12. package/dist/public/js/app-sandbox.js +42 -0
  13. package/dist/public/js/services/generator-prompts-base.js +40 -3
  14. package/dist/public/js/services/living.js +506 -0
  15. package/dist/public/js/services/notebook-plan.js +211 -0
  16. package/dist/public/js/services/notebook.js +149 -0
  17. package/dist/public/js/services/organisms.js +10 -2
  18. package/dist/public/js/services/tracked-responses.js +79 -0
  19. package/dist/public/js/utils.js +15 -0
  20. package/dist/public/spa.html +20 -1
  21. package/dist/public/types/aimeat-globals.d.ts +4 -0
  22. package/dist/public/views/app-grant.js +210 -0
  23. package/dist/public/views/business.js +8 -1
  24. package/dist/public/views/how-it-works.js +5 -1
  25. package/dist/public/views/landing-node-totals.js +71 -0
  26. package/dist/public/views/landing.js +88 -79
  27. package/dist/public/views/profile/access-tab.js +75 -0
  28. package/dist/public/views/profile/agents-tab.js +3 -2
  29. package/dist/public/views/profile/apps-tab.js +17 -0
  30. package/dist/public/views/profile/inbox-tab.js +204 -7
  31. package/dist/public/views/profile/landing-page.js +255 -163
  32. package/dist/public/views/profile/living-tab.js +480 -0
  33. package/dist/public/views/profile/notebook-card.js +489 -0
  34. package/dist/public/views/profile/notebook-helpers.js +48 -0
  35. package/dist/public/views/profile/notebook-tab.js +40 -260
  36. package/dist/public/views/profile.js +2 -0
  37. package/dist/scripts/reassign-apps.d.ts +2 -0
  38. package/dist/scripts/reassign-apps.d.ts.map +1 -0
  39. package/dist/scripts/reassign-apps.js +99 -0
  40. package/dist/scripts/reassign-apps.js.map +1 -0
  41. package/dist/scripts/screenshot-worker.d.ts +2 -0
  42. package/dist/scripts/screenshot-worker.d.ts.map +1 -0
  43. package/dist/scripts/screenshot-worker.js +14 -0
  44. package/dist/scripts/screenshot-worker.js.map +1 -0
  45. package/dist/src/auth/jwt.d.ts +2 -0
  46. package/dist/src/auth/jwt.d.ts.map +1 -1
  47. package/dist/src/auth/jwt.js +2 -0
  48. package/dist/src/auth/jwt.js.map +1 -1
  49. package/dist/src/auth/middleware.d.ts.map +1 -1
  50. package/dist/src/auth/middleware.js +6 -2
  51. package/dist/src/auth/middleware.js.map +1 -1
  52. package/dist/src/cli/init-wizard.d.ts +7 -3
  53. package/dist/src/cli/init-wizard.d.ts.map +1 -1
  54. package/dist/src/cli/init-wizard.js +50 -5
  55. package/dist/src/cli/init-wizard.js.map +1 -1
  56. package/dist/src/cli/screenshot-worker.d.ts +2 -0
  57. package/dist/src/cli/screenshot-worker.d.ts.map +1 -0
  58. package/dist/src/cli/screenshot-worker.js +156 -0
  59. package/dist/src/cli/screenshot-worker.js.map +1 -0
  60. package/dist/src/config.d.ts +23 -0
  61. package/dist/src/config.d.ts.map +1 -1
  62. package/dist/src/config.js +28 -0
  63. package/dist/src/config.js.map +1 -1
  64. package/dist/src/generated/api-types.d.ts +2756 -1285
  65. package/dist/src/generated/api-types.d.ts.map +1 -1
  66. package/dist/src/generated/prisma-postgres/edge.js +21 -3
  67. package/dist/src/generated/prisma-postgres/index-browser.js +18 -0
  68. package/dist/src/generated/prisma-postgres/index.d.ts +1648 -155
  69. package/dist/src/generated/prisma-postgres/index.js +21 -3
  70. package/dist/src/generated/prisma-postgres/package.json +1 -1
  71. package/dist/src/generated/prisma-postgres/schema.prisma +21 -0
  72. package/dist/src/generated/prisma-postgres/wasm.js +21 -3
  73. package/dist/src/index.js +8 -0
  74. package/dist/src/index.js.map +1 -1
  75. package/dist/src/mcp/apps.d.ts +2 -0
  76. package/dist/src/mcp/apps.d.ts.map +1 -1
  77. package/dist/src/mcp/apps.js +32 -2
  78. package/dist/src/mcp/apps.js.map +1 -1
  79. package/dist/src/mcp/extensions.d.ts +1 -0
  80. package/dist/src/mcp/extensions.d.ts.map +1 -1
  81. package/dist/src/mcp/extensions.js +10 -1
  82. package/dist/src/mcp/extensions.js.map +1 -1
  83. package/dist/src/mcp/workspaces.d.ts.map +1 -1
  84. package/dist/src/mcp/workspaces.js +4 -1
  85. package/dist/src/mcp/workspaces.js.map +1 -1
  86. package/dist/src/middleware/rate-limit.d.ts +10 -0
  87. package/dist/src/middleware/rate-limit.d.ts.map +1 -1
  88. package/dist/src/middleware/rate-limit.js +24 -4
  89. package/dist/src/middleware/rate-limit.js.map +1 -1
  90. package/dist/src/middleware/subdomain.d.ts +15 -3
  91. package/dist/src/middleware/subdomain.d.ts.map +1 -1
  92. package/dist/src/middleware/subdomain.js +35 -12
  93. package/dist/src/middleware/subdomain.js.map +1 -1
  94. package/dist/src/routes/admin.js +4 -2
  95. package/dist/src/routes/admin.js.map +1 -1
  96. package/dist/src/routes/agent-webhook.d.ts.map +1 -1
  97. package/dist/src/routes/agent-webhook.js +3 -2
  98. package/dist/src/routes/agent-webhook.js.map +1 -1
  99. package/dist/src/routes/app-grants.d.ts +35 -0
  100. package/dist/src/routes/app-grants.d.ts.map +1 -0
  101. package/dist/src/routes/app-grants.js +370 -0
  102. package/dist/src/routes/app-grants.js.map +1 -0
  103. package/dist/src/routes/apps.d.ts +18 -0
  104. package/dist/src/routes/apps.d.ts.map +1 -1
  105. package/dist/src/routes/apps.js +232 -13
  106. package/dist/src/routes/apps.js.map +1 -1
  107. package/dist/src/routes/extensions.d.ts +2 -0
  108. package/dist/src/routes/extensions.d.ts.map +1 -1
  109. package/dist/src/routes/extensions.js +18 -9
  110. package/dist/src/routes/extensions.js.map +1 -1
  111. package/dist/src/routes/foundry.d.ts.map +1 -1
  112. package/dist/src/routes/foundry.js +13 -1
  113. package/dist/src/routes/foundry.js.map +1 -1
  114. package/dist/src/routes/generator.d.ts.map +1 -1
  115. package/dist/src/routes/generator.js +13 -1
  116. package/dist/src/routes/generator.js.map +1 -1
  117. package/dist/src/routes/ghii.d.ts.map +1 -1
  118. package/dist/src/routes/ghii.js +20 -10
  119. package/dist/src/routes/ghii.js.map +1 -1
  120. package/dist/src/routes/librarian.d.ts.map +1 -1
  121. package/dist/src/routes/librarian.js +64 -2
  122. package/dist/src/routes/librarian.js.map +1 -1
  123. package/dist/src/routes/libs.d.ts +15 -0
  124. package/dist/src/routes/libs.d.ts.map +1 -1
  125. package/dist/src/routes/libs.js +301 -2
  126. package/dist/src/routes/libs.js.map +1 -1
  127. package/dist/src/routes/living.d.ts +18 -0
  128. package/dist/src/routes/living.d.ts.map +1 -0
  129. package/dist/src/routes/living.js +63 -0
  130. package/dist/src/routes/living.js.map +1 -0
  131. package/dist/src/routes/memory.d.ts.map +1 -1
  132. package/dist/src/routes/memory.js +4 -1
  133. package/dist/src/routes/memory.js.map +1 -1
  134. package/dist/src/routes/messages.d.ts +2 -0
  135. package/dist/src/routes/messages.d.ts.map +1 -1
  136. package/dist/src/routes/messages.js +16 -87
  137. package/dist/src/routes/messages.js.map +1 -1
  138. package/dist/src/routes/oauth-login.d.ts +22 -0
  139. package/dist/src/routes/oauth-login.d.ts.map +1 -0
  140. package/dist/src/routes/oauth-login.js +219 -0
  141. package/dist/src/routes/oauth-login.js.map +1 -0
  142. package/dist/src/routes/organisms.d.ts.map +1 -1
  143. package/dist/src/routes/organisms.js +4 -1
  144. package/dist/src/routes/organisms.js.map +1 -1
  145. package/dist/src/routes/portal.d.ts +3 -0
  146. package/dist/src/routes/portal.d.ts.map +1 -1
  147. package/dist/src/routes/portal.js +20 -12
  148. package/dist/src/routes/portal.js.map +1 -1
  149. package/dist/src/routes/public-stats.d.ts +3 -0
  150. package/dist/src/routes/public-stats.d.ts.map +1 -1
  151. package/dist/src/routes/public-stats.js +48 -0
  152. package/dist/src/routes/public-stats.js.map +1 -1
  153. package/dist/src/routes/storage-files.js +10 -10
  154. package/dist/src/routes/storage-files.js.map +1 -1
  155. package/dist/src/routes/subdomains.d.ts +33 -0
  156. package/dist/src/routes/subdomains.d.ts.map +1 -1
  157. package/dist/src/routes/subdomains.js +182 -9
  158. package/dist/src/routes/subdomains.js.map +1 -1
  159. package/dist/src/routes/tracked-responses.d.ts +27 -0
  160. package/dist/src/routes/tracked-responses.d.ts.map +1 -0
  161. package/dist/src/routes/tracked-responses.js +160 -0
  162. package/dist/src/routes/tracked-responses.js.map +1 -0
  163. package/dist/src/routes/upload.js +8 -0
  164. package/dist/src/routes/upload.js.map +1 -1
  165. package/dist/src/server-bootstrap/routes-loader.d.ts.map +1 -1
  166. package/dist/src/server-bootstrap/routes-loader.js +33 -0
  167. package/dist/src/server-bootstrap/routes-loader.js.map +1 -1
  168. package/dist/src/server-bootstrap/service-init.d.ts.map +1 -1
  169. package/dist/src/server-bootstrap/service-init.js +4 -0
  170. package/dist/src/server-bootstrap/service-init.js.map +1 -1
  171. package/dist/src/server-bootstrap/static-files.d.ts +8 -0
  172. package/dist/src/server-bootstrap/static-files.d.ts.map +1 -1
  173. package/dist/src/server-bootstrap/static-files.js +52 -3
  174. package/dist/src/server-bootstrap/static-files.js.map +1 -1
  175. package/dist/src/server.d.ts.map +1 -1
  176. package/dist/src/server.js +5 -0
  177. package/dist/src/server.js.map +1 -1
  178. package/dist/src/services/core-jobs.d.ts.map +1 -1
  179. package/dist/src/services/core-jobs.js +5 -0
  180. package/dist/src/services/core-jobs.js.map +1 -1
  181. package/dist/src/services/event-bus.d.ts +10 -0
  182. package/dist/src/services/event-bus.d.ts.map +1 -1
  183. package/dist/src/services/event-bus.js +9 -0
  184. package/dist/src/services/event-bus.js.map +1 -1
  185. package/dist/src/services/generator-prompts/resolvers.d.ts +2 -0
  186. package/dist/src/services/generator-prompts/resolvers.d.ts.map +1 -1
  187. package/dist/src/services/generator-prompts/resolvers.js +22 -3
  188. package/dist/src/services/generator-prompts/resolvers.js.map +1 -1
  189. package/dist/src/services/hooks.js +2 -2
  190. package/dist/src/services/hooks.js.map +1 -1
  191. package/dist/src/services/job-seeding.d.ts.map +1 -1
  192. package/dist/src/services/job-seeding.js +2 -0
  193. package/dist/src/services/job-seeding.js.map +1 -1
  194. package/dist/src/services/living-author-prompt.d.ts +14 -0
  195. package/dist/src/services/living-author-prompt.d.ts.map +1 -0
  196. package/dist/src/services/living-author-prompt.js +47 -0
  197. package/dist/src/services/living-author-prompt.js.map +1 -0
  198. package/dist/src/services/living-author.d.ts +55 -0
  199. package/dist/src/services/living-author.d.ts.map +1 -0
  200. package/dist/src/services/living-author.js +91 -0
  201. package/dist/src/services/living-author.js.map +1 -0
  202. package/dist/src/services/living-pulse.d.ts +29 -0
  203. package/dist/src/services/living-pulse.d.ts.map +1 -0
  204. package/dist/src/services/living-pulse.js +363 -0
  205. package/dist/src/services/living-pulse.js.map +1 -0
  206. package/dist/src/services/message-send.d.ts +24 -0
  207. package/dist/src/services/message-send.d.ts.map +1 -0
  208. package/dist/src/services/message-send.js +114 -0
  209. package/dist/src/services/message-send.js.map +1 -0
  210. package/dist/src/services/notebook-ai.d.ts +36 -0
  211. package/dist/src/services/notebook-ai.d.ts.map +1 -0
  212. package/dist/src/services/notebook-ai.js +51 -0
  213. package/dist/src/services/notebook-ai.js.map +1 -0
  214. package/dist/src/services/notebook-classify.d.ts +32 -5
  215. package/dist/src/services/notebook-classify.d.ts.map +1 -1
  216. package/dist/src/services/notebook-classify.js +59 -43
  217. package/dist/src/services/notebook-classify.js.map +1 -1
  218. package/dist/src/services/notebook-distribute-prompt.d.ts +16 -0
  219. package/dist/src/services/notebook-distribute-prompt.d.ts.map +1 -0
  220. package/dist/src/services/notebook-distribute-prompt.js +40 -0
  221. package/dist/src/services/notebook-distribute-prompt.js.map +1 -0
  222. package/dist/src/services/notebook-plan-prompt.d.ts +19 -0
  223. package/dist/src/services/notebook-plan-prompt.d.ts.map +1 -0
  224. package/dist/src/services/notebook-plan-prompt.js +54 -0
  225. package/dist/src/services/notebook-plan-prompt.js.map +1 -0
  226. package/dist/src/services/notebook-plan.d.ts +70 -0
  227. package/dist/src/services/notebook-plan.d.ts.map +1 -0
  228. package/dist/src/services/notebook-plan.js +118 -0
  229. package/dist/src/services/notebook-plan.js.map +1 -0
  230. package/dist/src/services/oidc-client.d.ts +3 -0
  231. package/dist/src/services/oidc-client.d.ts.map +1 -1
  232. package/dist/src/services/oidc-client.js +17 -2
  233. package/dist/src/services/oidc-client.js.map +1 -1
  234. package/dist/src/services/prompt-defaults.d.ts.map +1 -1
  235. package/dist/src/services/prompt-defaults.js +30 -0
  236. package/dist/src/services/prompt-defaults.js.map +1 -1
  237. package/dist/src/services/quota.d.ts +14 -1
  238. package/dist/src/services/quota.d.ts.map +1 -1
  239. package/dist/src/services/quota.js +28 -1
  240. package/dist/src/services/quota.js.map +1 -1
  241. package/dist/src/services/scheduler.d.ts +5 -0
  242. package/dist/src/services/scheduler.d.ts.map +1 -1
  243. package/dist/src/services/scheduler.js +4 -0
  244. package/dist/src/services/scheduler.js.map +1 -1
  245. package/dist/src/services/screenshot-capture.d.ts +28 -0
  246. package/dist/src/services/screenshot-capture.d.ts.map +1 -0
  247. package/dist/src/services/screenshot-capture.js +154 -0
  248. package/dist/src/services/screenshot-capture.js.map +1 -0
  249. package/dist/src/services/track-registry.d.ts +29 -0
  250. package/dist/src/services/track-registry.d.ts.map +1 -0
  251. package/dist/src/services/track-registry.js +61 -0
  252. package/dist/src/services/track-registry.js.map +1 -0
  253. package/dist/src/services/tracked-response.d.ts +98 -0
  254. package/dist/src/services/tracked-response.d.ts.map +1 -0
  255. package/dist/src/services/tracked-response.js +304 -0
  256. package/dist/src/services/tracked-response.js.map +1 -0
  257. package/dist/src/services/webhook-dispatcher.d.ts.map +1 -1
  258. package/dist/src/services/webhook-dispatcher.js +3 -2
  259. package/dist/src/services/webhook-dispatcher.js.map +1 -1
  260. package/dist/src/storage/interface.d.ts +19 -2
  261. package/dist/src/storage/interface.d.ts.map +1 -1
  262. package/dist/src/storage/interface.js.map +1 -1
  263. package/dist/src/storage/providers/mongodb/index.d.ts +16 -1
  264. package/dist/src/storage/providers/mongodb/index.d.ts.map +1 -1
  265. package/dist/src/storage/providers/mongodb/index.js +123 -0
  266. package/dist/src/storage/providers/mongodb/index.js.map +1 -1
  267. package/dist/src/storage/providers/sqlite/index.d.ts +10 -1
  268. package/dist/src/storage/providers/sqlite/index.d.ts.map +1 -1
  269. package/dist/src/storage/providers/sqlite/index.js +119 -6
  270. package/dist/src/storage/providers/sqlite/index.js.map +1 -1
  271. package/dist/src/storage/providers/sqlite/repos/identity.d.ts +1 -0
  272. package/dist/src/storage/providers/sqlite/repos/identity.d.ts.map +1 -1
  273. package/dist/src/storage/providers/sqlite/repos/identity.js +10 -4
  274. package/dist/src/storage/providers/sqlite/repos/identity.js.map +1 -1
  275. package/dist/src/storage/providers/sqlite/schema.d.ts +16 -3
  276. package/dist/src/storage/providers/sqlite/schema.d.ts.map +1 -1
  277. package/dist/src/storage/providers/sqlite/schema.js +31 -4
  278. package/dist/src/storage/providers/sqlite/schema.js.map +1 -1
  279. package/dist/src/storage/repositories/app-grant.repository.d.ts +22 -0
  280. package/dist/src/storage/repositories/app-grant.repository.d.ts.map +1 -0
  281. package/dist/src/storage/repositories/app-grant.repository.js +2 -0
  282. package/dist/src/storage/repositories/app-grant.repository.js.map +1 -0
  283. package/dist/src/storage/repositories/app.repository.d.ts +9 -0
  284. package/dist/src/storage/repositories/app.repository.d.ts.map +1 -1
  285. package/dist/src/storage/repositories/identity.repository.d.ts +1 -0
  286. package/dist/src/storage/repositories/identity.repository.d.ts.map +1 -1
  287. package/dist/src/storage/repositories/index.d.ts +1 -0
  288. package/dist/src/storage/repositories/index.d.ts.map +1 -1
  289. package/dist/src/utils/env-config.d.ts +5 -2
  290. package/dist/src/utils/env-config.d.ts.map +1 -1
  291. package/dist/src/utils/env-config.js +63 -2
  292. package/dist/src/utils/env-config.js.map +1 -1
  293. package/dist/src/utils/env-validator.d.ts +6 -3
  294. package/dist/src/utils/env-validator.d.ts.map +1 -1
  295. package/dist/src/utils/env-validator.js +14 -3
  296. package/dist/src/utils/env-validator.js.map +1 -1
  297. package/dist/src/utils/url-validator.d.ts +15 -0
  298. package/dist/src/utils/url-validator.d.ts.map +1 -1
  299. package/dist/src/utils/url-validator.js +120 -30
  300. package/dist/src/utils/url-validator.js.map +1 -1
  301. package/dist/static/app-catalog.html +505 -30
  302. package/dist/static/app-login.js +67 -0
  303. package/dist/static/app-silent.html +14 -0
  304. package/dist/static/app-silent.js +32 -0
  305. package/package.json +3 -1
  306. package/prisma/schema.postgres.prisma +39 -18
  307. package/prisma/schema.prisma +21 -0
@@ -0,0 +1,42 @@
1
+ /**
2
+ * @file public/js/app-sandbox.js
3
+ * @description Open a user-published app from anywhere in the SPA. Apps always open TOP-LEVEL
4
+ * in a new tab via the apex `?mode=inline` URL — a clean, full-screen page (no overlay/X).
5
+ * The server decides isolation:
6
+ * - app origin OFF → the app is served inline on the apex (same host as the SPA), so the
7
+ * user's session carries into the app (it can refresh + read its own data — "logged in");
8
+ * - app origin ON → the apex 301s to `apps.<domain>`, an isolated origin with no ambient
9
+ * session (apps then use the explicit grant flow).
10
+ * So "logged into aimeat.io ⇒ apps are logged in" is the OFF (default) behaviour; ON is the
11
+ * opt-in hardened mode for multi-user/public nodes.
12
+ * @structure isAppHtmlUrl() route matcher · openAppSandboxed() top-level opener.
13
+ * @usage import { openAppSandboxed, isAppHtmlUrl } from '/js/app-sandbox.js'
14
+ * @version-history
15
+ * v1.0.0 — 2026-06-20 — Initial (H-2 app-origin isolation, Phase 0).
16
+ * v1.1.0 — 2026-06-20 — App origin live → open apps top-level; opaque-sandbox overlay fallback.
17
+ * v2.0.0 — 2026-06-20 — Always open apps TOP-LEVEL (the opaque sandbox gave apps origin `null`,
18
+ * breaking their storage/API and forcing a re-login). Isolation is now the server's job via
19
+ * the app origin; with it off, apps run on the apex and inherit the user's session.
20
+ */
21
+
22
+ /** Matches the published-app HTML route: /v1/apps/<owner>/<file> (optionally ?mode=inline). */
23
+ export function isAppHtmlUrl(href) {
24
+ if (!href) return false;
25
+ let path;
26
+ try {
27
+ path = new URL(href, window.location.href).pathname;
28
+ } catch {
29
+ return false;
30
+ }
31
+ // /v1/apps/<owner>/<file> — two path segments after /v1/apps/. The bare collection
32
+ // (/v1/apps) and single-segment forms are API/JSON, not runnable HTML.
33
+ return /^\/v1\/apps\/[^/]+\/[^/]+$/.test(path);
34
+ }
35
+
36
+ /**
37
+ * Open a published app in a new top-level tab. `url` is the apex `?mode=inline` URL; the server
38
+ * either serves it inline (session inherited) or 301s it to the isolated app origin.
39
+ */
40
+ export function openAppSandboxed(url, _name) {
41
+ window.open(url, '_blank', 'noopener');
42
+ }
@@ -13,6 +13,8 @@
13
13
  * import { AIMEAT_CONTEXT, INSTRUCTION_DISCLAIMER, COMPONENT_TEMPLATES } from '/js/services/generator-prompts-base.js';
14
14
  * @version-history
15
15
  * v1.0.0 — 2026-03-22 — Extracted from generator-prompts.js
16
+ * v1.1.0 — 2026-06-20 — App-build prompt: teach H-2 app-origin isolation (no ambient session;
17
+ * public data via /v1/..., private data via the app-grant PKCE flow + scoped Bearer token).
16
18
  */
17
19
 
18
20
  export const AIMEAT_CONTEXT = `
@@ -600,9 +602,44 @@ Create an AIMEAT App (HTML/JS) for: ${label}
600
602
 
601
603
  ${context}
602
604
 
603
- ## CRITICAL: Authentication & API Calls
604
-
605
- The app runs on the SAME ORIGIN as the AIMEAT node. Use relative API paths (e.g., "/v1/ext/..."), NOT absolute URLs.
605
+ ## CRITICAL: App origin isolation & data access (read this first)
606
+
607
+ Your published app runs on an ISOLATED app origin (\`*.apps.<domain>\`, e.g. \`apps.aimeat.io\`) — a DIFFERENT browser origin from the AIMEAT node (\`aimeat.io\`). Consequences you MUST respect:
608
+ - The app has NO access to the user's AIMEAT login session, cookie, or localStorage. There is no ambient/owner session.
609
+ - NEVER call \`/v1/auth/refresh\` and NEVER fetch with \`credentials:'include'\` expecting the user's session — that pattern was removed for security (H-2) and now returns 401/403.
610
+ - Same-origin \`fetch('/v1/...')\` to the app origin still works (CORS \`*\`) for PUBLIC data that needs no auth — public memory (\`getPublic\`), the catalogue, public boards.
611
+ - For the user's OWN/PRIVATE data, you MUST use the explicit, revocable **app-grant** flow (OAuth-style, PKCE) below. The token you obtain is scoped and stored in YOUR OWN origin's localStorage; you call APIs with \`Authorization: Bearer <that token>\` — never the user's session.
612
+ - Apps that use the user's OpenRouter key via cortex (\`AIMEAT.ai.complete()\`) are UNAFFECTED — that path is separate; use it as documented.
613
+
614
+ ### App-grant flow for private data (only when you need the user's own data)
615
+ 1. Send the user to the trusted apex to approve (PKCE \`code_challenge\` = base64url(SHA-256(verifier)), method S256):
616
+ \\\`\\\`\\\`javascript
617
+ // scope = space-separated agent scopes: memory:read memory:write memory:delete
618
+ // catalogue:read social:read social:write wallet:read knowledge:read
619
+ const verifier = base64url(crypto.getRandomValues(new Uint8Array(32)));
620
+ sessionStorage.setItem('pkce', verifier);
621
+ const challenge = base64url(new Uint8Array(await crypto.subtle.digest('SHA-256', new TextEncoder().encode(verifier))));
622
+ const url = 'https://aimeat.io/v1/app-grants/authorize'
623
+ + '?app=' + encodeURIComponent('<owner>/<file>') // your published app id
624
+ + '&response_type=code&scope=' + encodeURIComponent('memory:read')
625
+ + '&redirect_uri=' + encodeURIComponent(location.origin + location.pathname) // your URL on the app origin
626
+ + '&state=' + state + '&code_challenge=' + challenge + '&code_challenge_method=S256';
627
+ location.href = url;
628
+ \\\`\\\`\\\`
629
+ 2. On return, exchange the \`code\` for a scoped token (store it in YOUR localStorage):
630
+ \\\`\\\`\\\`javascript
631
+ const r = await fetch('https://aimeat.io/v1/app-grants/token', {
632
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
633
+ body: JSON.stringify({ grant_type: 'authorization_code', code, code_verifier: verifier, redirect_uri: location.origin + location.pathname })
634
+ }).then(x => x.json());
635
+ localStorage.setItem('aimeat_token', r.data.access_token); // scoped, revocable
636
+ localStorage.setItem('aimeat_refresh', r.data.refresh_token); // re-exchange with grant_type:'refresh_token'
637
+ \\\`\\\`\\\`
638
+ 3. Call AIMEAT APIs with that Bearer token (NOT the user's session):
639
+ \\\`\\\`\\\`javascript
640
+ await fetch('https://aimeat.io/v1/memory/my.key', { headers: { Authorization: 'Bearer ' + localStorage.getItem('aimeat_token') } });
641
+ \\\`\\\`\\\`
642
+ The user manages/revokes these tokens in Profile → Access → "Connected Apps". Request the FEWEST scopes you need.
606
643
 
607
644
  ### Library setup (copy this exactly — load BOTH libraries):
608
645
  \`\`\`javascript
@@ -0,0 +1,506 @@
1
+ /**
2
+ * @file living.js
3
+ * @description Living Documents client service (Phase 0 — no pulse yet). A living document is a
4
+ * workspace-backed markdown document ASSEMBLED from memory keys. Two surfaces:
5
+ * - Templates (personal, owner memory `living.template.{id}`): reusable charter+template
6
+ * skeletons authored/managed in the profile Living Documents tab.
7
+ * - Instances (workspace-backed): a template DEPLOYED into a workspace, which then accrues
8
+ * content per slot and renders as markdown. Each instance config snapshots templateId+version.
9
+ * Phase 0 is client-orchestrated over the generic memory/organism APIs (no-SSR): template CRUD,
10
+ * deploy, manual add-source / derive-slot, assemble + render. The pulse (workflow), AI authoring,
11
+ * per-slot versioning and the marketplace come in later phases — see
12
+ * docs/plans/2026-06-21-living-documents-plan.md.
13
+ * @structure
14
+ * - templates: listTemplates / getTemplate / saveTemplate / deleteTemplate / blankTemplate
15
+ * - deploy: deployTemplate(template, orgId, wsId)
16
+ * - instances: listInstances / readInstance / addSource / deriveSlotFromSources / setSlotContent
17
+ * - render: renderInstanceMarkdown(instance)
18
+ * @version-history
19
+ * v1.0.0 — 2026-06-21 — Phase 0: templates + deploy + manual derive + assemble/render.
20
+ */
21
+ import { api } from '/js/api.js';
22
+ import { createMemory, getMemory, listMemories, deleteMemory, librarianSearch } from '/js/services/memory.js';
23
+ import { wsRoot, saveManifest } from '/js/services/organisms.js';
24
+ import * as offersService from '/js/services/offers.js';
25
+ import { getTask } from '/js/services/agent-tasks.js';
26
+
27
+ const TEMPLATE_PREFIX = 'living.template.';
28
+ const SCHEMA = 'schema:living-document@1';
29
+
30
+ function rid(prefix) {
31
+ const r = (typeof crypto !== 'undefined' && crypto.randomUUID) ? crypto.randomUUID().slice(0, 8) : Math.random().toString(36).slice(2, 10);
32
+ return prefix + r;
33
+ }
34
+
35
+ // ── Templates (personal, owner memory) ──
36
+
37
+ /** A fresh, editable template skeleton. */
38
+ export function blankTemplate() {
39
+ return {
40
+ id: rid('lt-'),
41
+ title: '',
42
+ description: '',
43
+ visibility: 'private', // forward-compat: org/public when the marketplace lands
44
+ charter: { scope: '', cadence: 'daily', trust: { derive: 'auto' } },
45
+ template: [
46
+ { section: 'Overview', desc: 'A short, current summary of the topic.', slot: 'overview', kind: 'derived', rules: { max_words: 150 } },
47
+ ],
48
+ version: 1,
49
+ };
50
+ }
51
+
52
+ export async function listTemplates() {
53
+ const all = await listMemories();
54
+ return (Array.isArray(all) ? all : [])
55
+ .filter(m => typeof m.key === 'string' && m.key.startsWith(TEMPLATE_PREFIX))
56
+ .map(m => m.value)
57
+ .filter(v => v && typeof v === 'object')
58
+ .sort((a, b) => String(a.title || '').localeCompare(String(b.title || '')));
59
+ }
60
+
61
+ export async function getTemplate(id) {
62
+ const r = await getMemory(TEMPLATE_PREFIX + id);
63
+ return r?.data?.value || null;
64
+ }
65
+
66
+ export async function saveTemplate(tpl) {
67
+ const next = { ...tpl, updatedAt: new Date().toISOString() };
68
+ if (!next.createdAt) next.createdAt = next.updatedAt;
69
+ const resp = await createMemory(TEMPLATE_PREFIX + next.id, next, 'private');
70
+ if (resp?.ok === false) throw new Error(resp.error?.message || 'Could not save template');
71
+ return next;
72
+ }
73
+
74
+ export async function deleteTemplate(id) {
75
+ return deleteMemory(TEMPLATE_PREFIX + id);
76
+ }
77
+
78
+ /** AI-author a template from a plain-language need (POST /v1/living/author). `catalogue` grounds the
79
+ * agent suggestions. Slow AI call — full timeout, no retry. Returns { template, model }. */
80
+ export async function authorTemplate(need, catalogue) {
81
+ const resp = await api('/v1/living/author', {
82
+ method: 'POST',
83
+ body: JSON.stringify({ need, catalogue: Array.isArray(catalogue) ? catalogue : [] }),
84
+ timeoutMs: 1_800_000,
85
+ retries: 0,
86
+ });
87
+ if (resp?.ok === false) { const e = new Error(resp.error?.message || 'Author failed'); e.code = resp.error?.code; throw e; }
88
+ return resp?.data || null;
89
+ }
90
+
91
+ function yamlScalar(v) { const s = String(v); return (s === '' || /[:#]/.test(s)) ? JSON.stringify(s) : s; }
92
+
93
+ /** Minimal display-only YAML for the charter object (the "technical" view). Not a full serializer —
94
+ * it covers the bounded charter shape (strings, string arrays, nested objects). */
95
+ export function charterToYaml(obj, indent = 0) {
96
+ const pad = ' '.repeat(indent);
97
+ const lines = [];
98
+ for (const [k, v] of Object.entries(obj || {})) {
99
+ if (Array.isArray(v)) {
100
+ if (!v.length) { lines.push(`${pad}${k}: []`); continue; }
101
+ lines.push(`${pad}${k}:`);
102
+ for (const it of v) {
103
+ if (it && typeof it === 'object') { lines.push(`${pad} -`); lines.push(charterToYaml(it, indent + 2)); }
104
+ else lines.push(`${pad} - ${yamlScalar(it)}`);
105
+ }
106
+ } else if (v && typeof v === 'object') {
107
+ lines.push(`${pad}${k}:`);
108
+ lines.push(charterToYaml(v, indent + 1));
109
+ } else {
110
+ lines.push(`${pad}${k}: ${yamlScalar(v)}`);
111
+ }
112
+ }
113
+ return lines.join('\n');
114
+ }
115
+
116
+ // ── Keys for a deployed instance (workspace-backed) ──
117
+
118
+ const configKey = (loc) => `${wsRoot(loc.orgId, loc.wsId)}.living.${loc.docId}.latest`;
119
+ const slotKey = (loc, slotId) => `${wsRoot(loc.orgId, loc.wsId)}.living-slot.${loc.docId}__${slotId}.latest`;
120
+ const pendingKey = (loc, slotId) => `${wsRoot(loc.orgId, loc.wsId)}.living-pending.${loc.docId}__${slotId}.latest`;
121
+ const histKey = (loc, slotId) => `${wsRoot(loc.orgId, loc.wsId)}.living-hist.${loc.docId}__${slotId}.latest`;
122
+ const pagesKey = (loc, docId) => `${wsRoot(loc.orgId, loc.wsId)}.pages.${docId}.latest`;
123
+ const srcKey = (loc, srcId) => `${wsRoot(loc.orgId, loc.wsId)}.living-src.${loc.docId}__${srcId}.latest`;
124
+ const HIST_CAP = 20;
125
+
126
+ /** The objectTypes a workspace needs to host living documents (merged into its manifest on deploy). */
127
+ function livingObjectTypes() {
128
+ return [
129
+ { name: 'Living', namespace: 'living', schemaRef: SCHEMA, mode: 'document', backing: 'memory', writeRole: 'member', cardinality: 'many', versioned: true },
130
+ { name: 'Living Sources', namespace: 'living-src', schemaRef: 'schema:living-source@1', mode: 'records', backing: 'memory', writeRole: 'member', cardinality: 'many', versioned: false },
131
+ { name: 'Living Slots', namespace: 'living-slot', schemaRef: 'schema:living-slot@1', mode: 'document', backing: 'memory', writeRole: 'member', cardinality: 'many', versioned: true },
132
+ { name: 'Living Ledger', namespace: 'living-ledger', schemaRef: 'schema:living-ledger@1', mode: 'records', backing: 'memory', writeRole: 'member', cardinality: 'many', versioned: false },
133
+ { name: 'Living Pending', namespace: 'living-pending', schemaRef: 'schema:living-slot@1', mode: 'document', backing: 'memory', writeRole: 'member', cardinality: 'many', versioned: false },
134
+ { name: 'Living History', namespace: 'living-hist', schemaRef: 'schema:living-hist@1', mode: 'document', backing: 'memory', writeRole: 'member', cardinality: 'many', versioned: false },
135
+ { name: 'Pages', namespace: 'pages', schemaRef: 'schema:document@1', mode: 'document', backing: 'memory', writeRole: 'member', cardinality: 'many', versioned: true },
136
+ ];
137
+ }
138
+
139
+ /** Public: ensure a workspace's manifest has the living object types (used by deploy + pulse). */
140
+ export async function ensureLivingTypes(orgId, wsId) { return ensureLivingManifest(orgId, wsId); }
141
+
142
+ async function ensureLivingManifest(orgId, wsId) {
143
+ let manifest = null;
144
+ try { manifest = (await getMemory(`${wsRoot(orgId, wsId)}.meta.manifest`))?.data?.value || null; } catch { /* none */ }
145
+ const base = manifest || { manifestVersion: '1.0', id: orgId, name: 'Workspace', kind: 'project', status: 'active', objectTypes: [] };
146
+ const have = new Set((base.objectTypes || []).map(o => o.namespace));
147
+ const merged = [...(base.objectTypes || []), ...livingObjectTypes().filter(o => !have.has(o.namespace))];
148
+ if (merged.length !== (base.objectTypes || []).length || !manifest) {
149
+ await saveManifest(orgId, wsId, { ...base, objectTypes: merged });
150
+ }
151
+ }
152
+
153
+ // ── Deploy a template into a workspace ──
154
+
155
+ /**
156
+ * Deploy a template into an existing workspace, creating a living instance.
157
+ * @returns {Promise<{orgId:string, wsId:string, docId:string}>}
158
+ */
159
+ export async function deployTemplate(template, orgId, wsId) {
160
+ if (!orgId || !wsId) throw new Error('Pick a workspace to deploy into');
161
+ await ensureLivingManifest(orgId, wsId);
162
+ const loc = { orgId, wsId, docId: rid('ld-') };
163
+ const config = {
164
+ id: loc.docId,
165
+ type: 'living-config',
166
+ templateId: template.id,
167
+ templateVersion: template.version || 1,
168
+ title: template.title || 'Living document',
169
+ charter: template.charter || {},
170
+ template: template.template || [],
171
+ status: { version: 1, last_pulse: null, health: 'new', paused: false, cost: 0 },
172
+ createdAt: new Date().toISOString(),
173
+ };
174
+ const resp = await createMemory(configKey(loc), config, 'private');
175
+ if (resp?.ok === false) throw new Error(resp.error?.message || 'Could not deploy');
176
+ return loc;
177
+ }
178
+
179
+ // ── Instances ──
180
+
181
+ /** Parse a living config key → loc, or null. e.g. organism.{org}.w.{ws}.living.{docId}.latest */
182
+ function parseConfigKey(key) {
183
+ const m = /^organism\.(.+?)\.w\.(.+?)\.living\.([^.]+)\.latest$/.exec(key);
184
+ return m ? { orgId: m[1], wsId: m[2], docId: m[3] } : null;
185
+ }
186
+
187
+ /** Every living instance across all the owner's workspaces (for the profile tab list). */
188
+ export async function listInstances() {
189
+ const all = await listMemories();
190
+ const out = [];
191
+ for (const m of (Array.isArray(all) ? all : [])) {
192
+ const loc = typeof m.key === 'string' ? parseConfigKey(m.key) : null;
193
+ if (loc && m.value && m.value.type === 'living-config') out.push({ loc, config: m.value, updatedAt: m.updated_at || m.created_at });
194
+ }
195
+ return out.sort((a, b) => +new Date(b.updatedAt || 0) - +new Date(a.updatedAt || 0));
196
+ }
197
+
198
+ /** Read one instance: config + its sources + slot derivations, keyed by slotId. */
199
+ export async function readInstance(orgId, wsId, docId) {
200
+ const loc = { orgId, wsId, docId };
201
+ const all = await listMemories();
202
+ const items = Array.isArray(all) ? all : [];
203
+ const config = items.find(m => m.key === configKey(loc))?.value || null;
204
+ if (!config) return null;
205
+ const slotPrefix = `${wsRoot(orgId, wsId)}.living-slot.${docId}__`;
206
+ const pendingPrefix = `${wsRoot(orgId, wsId)}.living-pending.${docId}__`;
207
+ const histPrefix = `${wsRoot(orgId, wsId)}.living-hist.${docId}__`;
208
+ const srcPrefix = `${wsRoot(orgId, wsId)}.living-src.${docId}__`;
209
+ const slots = {};
210
+ const pending = {};
211
+ const history = {};
212
+ const sources = [];
213
+ for (const m of items) {
214
+ if (typeof m.key !== 'string') continue;
215
+ if (m.key.startsWith(histPrefix) && m.value) history[m.value.slot] = m.value.versions || [];
216
+ else if (m.key.startsWith(pendingPrefix) && m.value) pending[m.value.slot] = m.value;
217
+ else if (m.key.startsWith(slotPrefix) && m.value) slots[m.value.slot] = m.value;
218
+ else if (m.key.startsWith(srcPrefix) && m.value) sources.push(m.value);
219
+ }
220
+ return { loc, config, slots, pending, history, sources };
221
+ }
222
+
223
+ /** Add a raw source to a slot. `data` (optional {label?, value}) feeds aggregate-slot charts. */
224
+ export async function addSource(loc, slotId, src) {
225
+ const { text = '', origin = '', producer = 'manual', data = null } = src || {};
226
+ const id = rid('s-');
227
+ const value = { id, slot: slotId, text, origin, producer, data, active: true, addedAt: new Date().toISOString() };
228
+ const resp = await createMemory(srcKey(loc, id), value, 'private');
229
+ if (resp?.ok === false) throw new Error(resp.error?.message || 'Could not add source');
230
+ return value;
231
+ }
232
+
233
+ /** Approve a pending (gated) delta: promote it to the live derivation and clear the pending entry. */
234
+ export async function approvePending(loc, slotId, pendingValue) {
235
+ await setSlotContent(loc, slotId, pendingValue?.markdown || '', pendingValue?.derivedFrom || []);
236
+ await deleteMemory(pendingKey(loc, slotId)).catch(() => {});
237
+ }
238
+
239
+ /** Reject a pending (gated) delta: discard it. */
240
+ export async function rejectPending(loc, slotId) {
241
+ await deleteMemory(pendingKey(loc, slotId)).catch(() => {});
242
+ }
243
+
244
+ /**
245
+ * Freeze a personalized snapshot: compose the chosen per-section versions (or the current ones) into a
246
+ * static markdown document saved to the workspace's normal `pages` space (a plain, non-living doc).
247
+ * @param {object} selection { [slotId]: versionEntry } picked from the timeline; missing → current.
248
+ */
249
+ export async function saveSnapshot(loc, config, selection) {
250
+ const inst = await readInstance(loc.orgId, loc.wsId, loc.docId);
251
+ const parts = [`# ${config.title || 'Living document'} — snapshot`];
252
+ if (config.charter?.scope) parts.push('', `_${config.charter.scope}_`);
253
+ for (const sec of (config.template || [])) {
254
+ parts.push('', `## ${sec.section || sec.slot}`);
255
+ const chosen = selection?.[sec.slot];
256
+ const md = (chosen?.markdown) ?? (inst.slots[sec.slot]?.markdown) ?? '_empty_';
257
+ parts.push('', md);
258
+ }
259
+ const markdown = parts.join('\n');
260
+ const docId = rid('snap-');
261
+ await ensureLivingManifest(loc.orgId, loc.wsId); // also ensures the 'pages' objectType
262
+ const resp = await createMemory(pagesKey(loc, docId), { id: docId, title: `${config.title || 'Living'} — snapshot`, markdown }, 'private');
263
+ if (resp?.ok === false) throw new Error(resp.error?.message || 'Could not save snapshot');
264
+ return { docId, markdown };
265
+ }
266
+
267
+ /** Append a version to a slot's capped history (newest first). Best-effort. */
268
+ async function appendHistory(loc, slotId, entry) {
269
+ try {
270
+ const cur = (await getMemory(histKey(loc, slotId)))?.data?.value;
271
+ const versions = [entry, ...((cur?.versions) || [])].slice(0, HIST_CAP);
272
+ await createMemory(histKey(loc, slotId), { slot: slotId, versions }, 'private');
273
+ } catch { /* history is best-effort */ }
274
+ }
275
+
276
+ /** Write a slot derivation directly (manual content or an applied delta) + record a history version. */
277
+ export async function setSlotContent(loc, slotId, markdown, derivedFrom, producedBy) {
278
+ const producedAt = new Date().toISOString();
279
+ const value = { slot: slotId, version: 1, markdown: markdown || '', derivedFrom: derivedFrom || [], producedAt, producedBy: producedBy || 'human' };
280
+ const resp = await createMemory(slotKey(loc, slotId), value, 'private');
281
+ if (resp?.ok === false) throw new Error(resp.error?.message || 'Could not set slot content');
282
+ await appendHistory(loc, slotId, { markdown: value.markdown, producedAt, producedBy: value.producedBy, derivedFrom: value.derivedFrom });
283
+ return value;
284
+ }
285
+
286
+ /** Phase 0 "derive": compose a slot's active sources into its markdown (a bullet list, with origins).
287
+ * The real pulse (Phase 2) replaces this with an AI refinement step; the interface stays the same. */
288
+ export async function deriveSlotFromSources(loc, slotId, sources) {
289
+ const active = (sources || []).filter(s => s.slot === slotId && s.active !== false);
290
+ const md = active.length
291
+ ? active.map(s => `- ${s.text}${s.origin ? ` 〔${s.origin}〕` : ''}`).join('\n')
292
+ : '_No sources yet._';
293
+ return setSlotContent(loc, slotId, md, active.map(s => s.id));
294
+ }
295
+
296
+ // ── Ledger ──
297
+
298
+ const ledgerKey = (loc, ts) => `${wsRoot(loc.orgId, loc.wsId)}.living-ledger.${loc.docId}__${ts}.latest`;
299
+
300
+ async function addLedger(loc, event) {
301
+ const ts = Date.now() + '-' + Math.random().toString(36).slice(2, 6);
302
+ try { await createMemory(ledgerKey(loc, ts), { ...event, at: new Date().toISOString() }, 'private'); } catch { /* best-effort */ }
303
+ }
304
+
305
+ /** Recent ledger events for an instance, newest first. */
306
+ export async function listLedger(orgId, wsId, docId, limit = 20) {
307
+ const all = await listMemories();
308
+ const prefix = `${wsRoot(orgId, wsId)}.living-ledger.${docId}__`;
309
+ return (Array.isArray(all) ? all : [])
310
+ .filter(m => typeof m.key === 'string' && m.key.startsWith(prefix) && m.value)
311
+ .map(m => m.value)
312
+ .sort((a, b) => +new Date(b.at || 0) - +new Date(a.at || 0))
313
+ .slice(0, limit);
314
+ }
315
+
316
+ // ── Pulse: refresh each slot (gather → re-derive), record ledger + cost + last_pulse ──
317
+
318
+ const PULSE_AI_TIMEOUT_MS = 1_800_000;
319
+ const DELEGATE_TIMEOUT_MS = 900_000;
320
+
321
+ /** Await a dispatched agent task to a terminal state (resolves the task on done; throws otherwise). */
322
+ function awaitTask(agentName, taskId, onStatus) {
323
+ return new Promise((resolve, reject) => {
324
+ const deadline = Date.now() + DELEGATE_TIMEOUT_MS;
325
+ let settled = false;
326
+ const cleanup = () => { clearInterval(timer); window.removeEventListener('aimeat-live-update', onLive); };
327
+ async function check() {
328
+ if (settled) return;
329
+ let task = null;
330
+ try { const r = await getTask(agentName, taskId); task = r?.data?.task || r?.data || null; } catch { /* transient */ }
331
+ const st = task?.status;
332
+ if (st) onStatus?.(st);
333
+ if (st === 'done') { settled = true; cleanup(); resolve(task); return; }
334
+ if (st === 'failed' || st === 'stalled') { settled = true; cleanup(); reject(new Error('agent ' + st)); return; }
335
+ if (Date.now() > deadline) { settled = true; cleanup(); reject(new Error('TIMEOUT')); }
336
+ }
337
+ const onLive = () => check();
338
+ const timer = setInterval(check, 5000);
339
+ window.addEventListener('aimeat-live-update', onLive);
340
+ check();
341
+ });
342
+ }
343
+
344
+ /** One AI completion via the owner's key (budget-enforced). Returns { content, costUsd }. */
345
+ async function aiRefine(prompt, systemPrompt) {
346
+ const resp = await api('/v1/ai/complete', {
347
+ method: 'POST',
348
+ body: JSON.stringify({ prompt, systemPrompt, app_id: 'living' }),
349
+ timeoutMs: PULSE_AI_TIMEOUT_MS, retries: 0,
350
+ });
351
+ if (resp?.ok === false) { const e = new Error(resp.error?.message || 'AI failed'); e.code = resp.error?.code; throw e; }
352
+ return { content: resp?.data?.content || '', costUsd: resp?.data?.usage?.cost_usd || 0 };
353
+ }
354
+
355
+ const DERIVE_SYSTEM =
356
+ 'You maintain ONE section of a living document. Rewrite the section so it is current, concise and well-organized, using ONLY the sources provided. Keep strictly to the section scope. Output clean markdown only — no preamble. Cite sources inline as 〔origin〕 where useful. Do not invent facts beyond the sources.';
357
+
358
+ /** Resolve a slot's assigned agent offer from the live feed. slot.agent is "agentName/offerId". */
359
+ function resolveSlotAgent(offersFeed, agentRef) {
360
+ if (!agentRef) return null;
361
+ const [agentName, offerId] = String(agentRef).split('/');
362
+ const agents = offersFeed?.data?.agents || [];
363
+ const entry = agents.find(a => a.agent === agentName);
364
+ const offer = entry?.offers?.find(o => o.id === offerId);
365
+ return entry && offer ? { entry, offer } : null;
366
+ }
367
+
368
+ /**
369
+ * Pulse one living instance: for each slot, gather fresh material (delegate to its agent, or search
370
+ * the owner's own material via the librarian), then AI re-derive the slot from its active sources.
371
+ * Records a ledger entry per slot and updates status (version, last_pulse, cost). Client-orchestrated;
372
+ * `onStatus(slotId, phase)` reports progress. Returns a summary.
373
+ * @param {{ onStatus?: (slotId: string, phase: string) => void }} [opts]
374
+ */
375
+ export async function pulseInstance(orgId, wsId, docId, opts = {}) {
376
+ const { onStatus } = opts;
377
+ const loc = { orgId, wsId, docId };
378
+ await ensureLivingManifest(orgId, wsId);
379
+ const inst = await readInstance(orgId, wsId, docId);
380
+ if (!inst) throw new Error('Instance not found');
381
+ const gated = inst.config.charter?.trust?.derive === 'gated';
382
+ const offersFeed = await offersService.listOffers().catch(() => null);
383
+
384
+ let costUsd = 0;
385
+ const results = [];
386
+ for (const sec of (inst.config.template || [])) {
387
+ const slotId = sec.slot;
388
+ let gathered = 0;
389
+ // 1. Gather
390
+ try {
391
+ const bound = resolveSlotAgent(offersFeed, sec.agent);
392
+ if (bound) {
393
+ onStatus?.(slotId, 'delegating');
394
+ const query = `${sec.section}. ${sec.desc || ''} Context: ${inst.config.charter?.scope || ''}`.trim();
395
+ const dispatch = await offersService.ask(bound.entry, bound.offer, query);
396
+ if (dispatch.kind === 'task' && dispatch.taskId) {
397
+ onStatus?.(slotId, 'waiting');
398
+ const task = await awaitTask(bound.entry.agent, dispatch.taskId, (s) => onStatus?.(slotId, s));
399
+ const content = await offersService.getDeliverableContent({ agentGaii: bound.entry.gaii, deliverableKey: task?.deliverableKey || task?.deliverable_key, taskId: dispatch.taskId });
400
+ if (content) { await addSource(loc, slotId, { text: content, origin: task?.deliverableKey || ('task:' + dispatch.taskId), producer: bound.entry.gaii }); gathered++; }
401
+ }
402
+ } else {
403
+ onStatus?.(slotId, 'searching');
404
+ const hits = await librarianSearch(`${sec.section} ${inst.config.charter?.scope || ''}`.trim(), 5, 'own').catch(() => []);
405
+ const existingOrigins = new Set(inst.sources.filter(s => s.slot === slotId).map(s => s.origin));
406
+ for (const h of hits) {
407
+ if (existingOrigins.has(h.key)) continue;
408
+ await addSource(loc, slotId, { text: h.snippet || h.title || h.key, origin: h.key, producer: h.producer });
409
+ gathered++;
410
+ }
411
+ }
412
+ } catch (e) {
413
+ await addLedger(loc, { event: 'gather-failed', slot: slotId, error: String(e.message || e) });
414
+ }
415
+
416
+ // 2. Re-derive from active sources
417
+ const fresh = await readInstance(orgId, wsId, docId);
418
+ const active = (fresh?.sources || []).filter(s => s.slot === slotId && s.active !== false);
419
+ if (active.length) {
420
+ try {
421
+ onStatus?.(slotId, 'deriving');
422
+ const srcList = active.map(s => `- ${s.text}${s.origin ? ` 〔${s.origin}〕` : ''}`).join('\n');
423
+ const prompt = `Section: ${sec.section}\nScope: ${sec.desc || ''}\nDocument scope: ${inst.config.charter?.scope || ''}\n\nSources:\n${srcList}\n\nWrite the section.`;
424
+ const { content, costUsd: c } = await aiRefine(prompt, DERIVE_SYSTEM);
425
+ costUsd += c;
426
+ if (gated) {
427
+ await createMemory(pendingKey(loc, slotId), { slot: slotId, markdown: content.trim(), derivedFrom: active.map(s => s.id), producedAt: new Date().toISOString(), producedBy: 'pulse', pending: true }, 'private');
428
+ await addLedger(loc, { event: 'pending', slot: slotId, sources: active.length, gathered });
429
+ } else {
430
+ await setSlotContent(loc, slotId, content.trim(), active.map(s => s.id), 'pulse');
431
+ await addLedger(loc, { event: 'slot-derived', slot: slotId, sources: active.length, gathered });
432
+ }
433
+ results.push({ slot: slotId, ok: true, gathered, gated });
434
+ } catch (e) {
435
+ await addLedger(loc, { event: 'derive-failed', slot: slotId, error: String(e.message || e) });
436
+ results.push({ slot: slotId, ok: false });
437
+ }
438
+ } else {
439
+ results.push({ slot: slotId, ok: true, gathered: 0, empty: true });
440
+ }
441
+ onStatus?.(slotId, 'done');
442
+ }
443
+
444
+ // 3. Update status
445
+ const cfg = inst.config;
446
+ const status = { ...(cfg.status || {}), version: (cfg.status?.version || 1) + 1, last_pulse: new Date().toISOString(), health: 'green', cost: Number(((cfg.status?.cost || 0) + costUsd).toFixed(4)) };
447
+ await createMemory(configKey(loc), { ...cfg, status }, 'private');
448
+ await addLedger(loc, { event: 'pulse', slots: results.length, costUsd: Number(costUsd.toFixed(4)) });
449
+ return { results, costUsd, status };
450
+ }
451
+
452
+ // ── Instance config controls (pause / cadence) + server pulse-due ──
453
+
454
+ async function saveInstanceConfig(loc, cfg) {
455
+ const resp = await createMemory(configKey(loc), cfg, 'private');
456
+ if (resp?.ok === false) throw new Error(resp.error?.message || 'Could not save');
457
+ return cfg;
458
+ }
459
+
460
+ /** Pause/resume an instance's unattended pulse (cost control). */
461
+ export async function setPaused(loc, cfg, paused) {
462
+ return saveInstanceConfig(loc, { ...cfg, status: { ...(cfg.status || {}), paused } });
463
+ }
464
+
465
+ /** Set the charter cadence ('hourly' | 'daily' | 'weekly', or a numeric cadence_minutes). */
466
+ export async function setCadence(loc, cfg, cadence) {
467
+ return saveInstanceConfig(loc, { ...cfg, charter: { ...(cfg.charter || {}), cadence } });
468
+ }
469
+
470
+ /** Trigger the server-side unattended pulse for the owner's own DUE instances now (the same logic the
471
+ * scheduler runs across all owners — exposed for on-demand refresh + testing). */
472
+ export async function pulseDueServer() {
473
+ const resp = await api('/v1/living/pulse-due', { method: 'POST', body: '{}', timeoutMs: 1_800_000, retries: 0 });
474
+ if (resp?.ok === false) { const e = new Error(resp.error?.message || 'Pulse failed'); e.code = resp.error?.code; throw e; }
475
+ return resp?.data || {};
476
+ }
477
+
478
+ /** Numeric data points for an aggregate slot (oldest→newest): from each source's `data.value`, else a
479
+ * number parsed from its text. Used to render a chart + table. */
480
+ export function aggregateData(sources, slotId) {
481
+ return (sources || [])
482
+ .filter(s => s.slot === slotId && s.active !== false)
483
+ .map(s => {
484
+ const value = Number(s.data?.value ?? parseFloat(String(s.text).replace(/[^0-9.\-]/g, '')));
485
+ const label = s.data?.label || String(s.text || '').split(':')[0]?.slice(0, 16) || '';
486
+ return { label, value, at: s.addedAt };
487
+ })
488
+ .filter(d => Number.isFinite(d.value))
489
+ .sort((a, b) => +new Date(a.at || 0) - +new Date(b.at || 0));
490
+ }
491
+
492
+ // ── Render ──
493
+
494
+ /** Assemble the instance into markdown by walking the template and dropping in each slot's content. */
495
+ export function renderInstanceMarkdown(instance) {
496
+ if (!instance?.config) return '';
497
+ const { config, slots = {} } = instance;
498
+ const parts = [`# ${config.title || 'Living document'}`];
499
+ if (config.charter?.scope) parts.push('', `_${config.charter.scope}_`);
500
+ for (const sec of (config.template || [])) {
501
+ parts.push('', `## ${sec.section || sec.slot}`);
502
+ const der = slots[sec.slot];
503
+ parts.push('', der?.markdown ? der.markdown : '_empty_');
504
+ }
505
+ return parts.join('\n');
506
+ }