@lobu/cli 6.0.1 → 7.0.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 (265) hide show
  1. package/README.md +20 -27
  2. package/dist/bundled-skills/lobu/SKILL.md +11 -11
  3. package/dist/commands/_lib/apply/apply-cmd.d.ts +38 -0
  4. package/dist/commands/_lib/apply/apply-cmd.d.ts.map +1 -1
  5. package/dist/commands/_lib/apply/apply-cmd.js +574 -40
  6. package/dist/commands/_lib/apply/apply-cmd.js.map +1 -1
  7. package/dist/commands/_lib/apply/client.d.ts +180 -1
  8. package/dist/commands/_lib/apply/client.d.ts.map +1 -1
  9. package/dist/commands/_lib/apply/client.js +308 -28
  10. package/dist/commands/_lib/apply/client.js.map +1 -1
  11. package/dist/commands/_lib/apply/desired-state.d.ts +134 -3
  12. package/dist/commands/_lib/apply/desired-state.d.ts.map +1 -1
  13. package/dist/commands/_lib/apply/desired-state.js +703 -89
  14. package/dist/commands/_lib/apply/desired-state.js.map +1 -1
  15. package/dist/commands/_lib/apply/diff.d.ts +61 -3
  16. package/dist/commands/_lib/apply/diff.d.ts.map +1 -1
  17. package/dist/commands/_lib/apply/diff.js +382 -92
  18. package/dist/commands/_lib/apply/diff.js.map +1 -1
  19. package/dist/commands/_lib/apply/prompt.d.ts +6 -0
  20. package/dist/commands/_lib/apply/prompt.d.ts.map +1 -1
  21. package/dist/commands/_lib/apply/prompt.js +16 -0
  22. package/dist/commands/_lib/apply/prompt.js.map +1 -1
  23. package/dist/commands/_lib/apply/render.d.ts +9 -0
  24. package/dist/commands/_lib/apply/render.d.ts.map +1 -1
  25. package/dist/commands/_lib/apply/render.js +80 -3
  26. package/dist/commands/_lib/apply/render.js.map +1 -1
  27. package/dist/commands/agent.d.ts +7 -0
  28. package/dist/commands/agent.d.ts.map +1 -1
  29. package/dist/commands/agent.js +65 -1
  30. package/dist/commands/agent.js.map +1 -1
  31. package/dist/commands/chat.d.ts +12 -9
  32. package/dist/commands/chat.d.ts.map +1 -1
  33. package/dist/commands/chat.js +125 -57
  34. package/dist/commands/chat.js.map +1 -1
  35. package/dist/commands/dev.d.ts +23 -7
  36. package/dist/commands/dev.d.ts.map +1 -1
  37. package/dist/commands/dev.js +197 -49
  38. package/dist/commands/dev.js.map +1 -1
  39. package/dist/commands/doctor.d.ts +1 -0
  40. package/dist/commands/doctor.d.ts.map +1 -1
  41. package/dist/commands/doctor.js +136 -0
  42. package/dist/commands/doctor.js.map +1 -1
  43. package/dist/commands/eval.d.ts +8 -0
  44. package/dist/commands/eval.d.ts.map +1 -1
  45. package/dist/commands/eval.js +72 -6
  46. package/dist/commands/eval.js.map +1 -1
  47. package/dist/commands/init.d.ts +22 -5
  48. package/dist/commands/init.d.ts.map +1 -1
  49. package/dist/commands/init.js +355 -182
  50. package/dist/commands/init.js.map +1 -1
  51. package/dist/commands/link.d.ts +11 -0
  52. package/dist/commands/link.d.ts.map +1 -0
  53. package/dist/commands/link.js +28 -0
  54. package/dist/commands/link.js.map +1 -0
  55. package/dist/commands/login.d.ts.map +1 -1
  56. package/dist/commands/login.js +14 -2
  57. package/dist/commands/login.js.map +1 -1
  58. package/dist/commands/memory/_lib/browser-auth-cmd.d.ts.map +1 -1
  59. package/dist/commands/memory/_lib/browser-auth-cmd.js +3 -3
  60. package/dist/commands/memory/_lib/browser-auth-cmd.js.map +1 -1
  61. package/dist/commands/memory/_lib/mcp.d.ts +2 -2
  62. package/dist/commands/memory/_lib/mcp.d.ts.map +1 -1
  63. package/dist/commands/memory/_lib/mcp.js +24 -12
  64. package/dist/commands/memory/_lib/mcp.js.map +1 -1
  65. package/dist/commands/memory/_lib/openclaw-auth.d.ts +1 -0
  66. package/dist/commands/memory/_lib/openclaw-auth.d.ts.map +1 -1
  67. package/dist/commands/memory/_lib/openclaw-auth.js +14 -3
  68. package/dist/commands/memory/_lib/openclaw-auth.js.map +1 -1
  69. package/dist/commands/memory/_lib/openclaw-cmd.js +1 -1
  70. package/dist/commands/memory/_lib/openclaw-cmd.js.map +1 -1
  71. package/dist/commands/memory/_lib/schema.d.ts +29 -2
  72. package/dist/commands/memory/_lib/schema.d.ts.map +1 -1
  73. package/dist/commands/memory/_lib/schema.js +121 -5
  74. package/dist/commands/memory/_lib/schema.js.map +1 -1
  75. package/dist/commands/memory/_lib/seed-cmd.d.ts.map +1 -1
  76. package/dist/commands/memory/_lib/seed-cmd.js +46 -24
  77. package/dist/commands/memory/_lib/seed-cmd.js.map +1 -1
  78. package/dist/commands/memory/run.d.ts.map +1 -1
  79. package/dist/commands/memory/run.js +2 -2
  80. package/dist/commands/memory/run.js.map +1 -1
  81. package/dist/commands/org.d.ts +4 -0
  82. package/dist/commands/org.d.ts.map +1 -1
  83. package/dist/commands/org.js +10 -0
  84. package/dist/commands/org.js.map +1 -1
  85. package/dist/commands/platforms/platform-prompts.d.ts +0 -1
  86. package/dist/commands/platforms/platform-prompts.d.ts.map +1 -1
  87. package/dist/commands/platforms/platform-prompts.js +54 -8
  88. package/dist/commands/platforms/platform-prompts.js.map +1 -1
  89. package/dist/commands/telemetry.d.ts +10 -0
  90. package/dist/commands/telemetry.d.ts.map +1 -0
  91. package/dist/commands/telemetry.js +68 -0
  92. package/dist/commands/telemetry.js.map +1 -0
  93. package/dist/commands/token.d.ts +9 -0
  94. package/dist/commands/token.d.ts.map +1 -1
  95. package/dist/commands/token.js +54 -0
  96. package/dist/commands/token.js.map +1 -1
  97. package/dist/commands/whoami.d.ts.map +1 -1
  98. package/dist/commands/whoami.js +1 -1
  99. package/dist/commands/whoami.js.map +1 -1
  100. package/dist/connectors/README.md +534 -0
  101. package/dist/connectors/__tests__/browser-scraper-utils.test.ts +186 -0
  102. package/dist/connectors/apple_health.ts +138 -0
  103. package/dist/connectors/apple_screen_time.ts +82 -0
  104. package/dist/connectors/browser-scraper-utils.ts +246 -0
  105. package/dist/connectors/capterra.ts +277 -0
  106. package/dist/connectors/g2.ts +290 -0
  107. package/dist/connectors/github.ts +1530 -0
  108. package/dist/connectors/glassdoor.ts +295 -0
  109. package/dist/connectors/gmaps.ts +197 -0
  110. package/dist/connectors/google_calendar.ts +641 -0
  111. package/dist/connectors/google_gmail.ts +754 -0
  112. package/dist/connectors/google_photos.ts +776 -0
  113. package/dist/connectors/google_play.ts +349 -0
  114. package/dist/connectors/hackernews.ts +471 -0
  115. package/dist/connectors/index.ts +28 -0
  116. package/dist/connectors/ios_appstore.ts +226 -0
  117. package/dist/connectors/linkedin.ts +494 -0
  118. package/dist/connectors/local_directory.ts +91 -0
  119. package/dist/connectors/microsoft_outlook.ts +410 -0
  120. package/dist/connectors/producthunt.ts +471 -0
  121. package/dist/connectors/reddit.ts +600 -0
  122. package/dist/connectors/revolut.ts +572 -0
  123. package/dist/connectors/rss.ts +448 -0
  124. package/dist/connectors/spotify.ts +590 -0
  125. package/dist/connectors/trustpilot.ts +203 -0
  126. package/dist/connectors/website.ts +629 -0
  127. package/dist/connectors/whatsapp.ts +1081 -0
  128. package/dist/connectors/whatsapp_local.ts +125 -0
  129. package/dist/connectors/x.ts +536 -0
  130. package/dist/connectors/youtube.ts +666 -0
  131. package/dist/db/migrations/00000000000000_baseline.sql +4867 -0
  132. package/dist/db/migrations/20260405193000_add_mcp_sessions.sql +33 -0
  133. package/dist/db/migrations/20260408120000_remove_system_connectors.sql +48 -0
  134. package/dist/db/migrations/20260408120001_optional_compiled_code.sql +6 -0
  135. package/dist/db/migrations/20260409110000_add_active_watcher_run_index.sql +9 -0
  136. package/dist/db/migrations/20260409130000_connector_default_config.sql +5 -0
  137. package/dist/db/migrations/20260410120000_add_agent_secrets.sql +25 -0
  138. package/dist/db/migrations/20260413170000_add_watcher_group_id.sql +67 -0
  139. package/dist/db/migrations/20260416120000_add_entity_wa_jid_index.sql +14 -0
  140. package/dist/db/migrations/20260417100000_add_entity_identities.sql +77 -0
  141. package/dist/db/migrations/20260418100000_add_auth_runs.sql +83 -0
  142. package/dist/db/migrations/20260418110000_add_runs_created_by_user.sql +18 -0
  143. package/dist/db/migrations/20260419120000_add_event_identity_indexes.sql +56 -0
  144. package/dist/db/migrations/20260420120000_extend_reserved_org_slugs.sql +56 -0
  145. package/dist/db/migrations/20260424030000_add_watcher_run_correlation.sql +52 -0
  146. package/dist/db/migrations/20260424130000_relax_events_client_id_fk.sql +47 -0
  147. package/dist/db/migrations/20260425100000_normalize_watcher_feedback.sql +91 -0
  148. package/dist/db/migrations/20260425120000_add_run_diagnostics.sql +20 -0
  149. package/dist/db/migrations/20260425130000_add_repair_agent_plumbing.sql +46 -0
  150. package/dist/db/migrations/20260426120000_entities_entity_type_fk.sql +101 -0
  151. package/dist/db/migrations/20260426130000_db_integrity_cleanup.sql +104 -0
  152. package/dist/db/migrations/20260426130001_db_integrity_cleanup_concurrent.sql +187 -0
  153. package/dist/db/migrations/20260427133000_events_created_by_nullable.sql +74 -0
  154. package/dist/db/migrations/20260427140000_identity_engine_indexes.sql +140 -0
  155. package/dist/db/migrations/20260427150000_drop_events_source_id.sql +177 -0
  156. package/dist/db/migrations/20260427160000_drop_dead_schema.sql +76 -0
  157. package/dist/db/migrations/20260427170000_market_founder_to_member.sql +364 -0
  158. package/dist/db/migrations/20260428040000_cascade_events_watchers_org_fk.sql +66 -0
  159. package/dist/db/migrations/20260428050000_add_runs_approved_input.sql +9 -0
  160. package/dist/db/migrations/20260429010000_auth_profile_tenant_scoped_fk.sql +79 -0
  161. package/dist/db/migrations/20260429060000_extend_runs_for_lobu_queue.sql +108 -0
  162. package/dist/db/migrations/20260429120000_agent_changed_notify.sql +97 -0
  163. package/dist/db/migrations/20260429120100_user_auth_profiles_and_model_prefs.sql +36 -0
  164. package/dist/db/migrations/20260429120200_fix_notify_old_keys.sql +130 -0
  165. package/dist/db/migrations/20260429130000_oauth_states_cli_sessions_rate_limits.sql +83 -0
  166. package/dist/db/migrations/20260429140000_phase8_grants_chat_connections_mcp_sessions.sql +84 -0
  167. package/dist/db/migrations/20260429140100_runs_priority_expires_at_retry_delay.sql +44 -0
  168. package/dist/db/migrations/20260429180000_drop_invalidatable_cache_triggers.sql +25 -0
  169. package/dist/db/migrations/20260430005614_agents_apply_fields.sql +21 -0
  170. package/dist/db/migrations/20260430022231_fix_connection_config_encryption.sql +69 -0
  171. package/dist/db/migrations/20260430151215_add_task_run_type.sql +77 -0
  172. package/dist/db/migrations/20260501000000_drop_cli_sessions.sql +27 -0
  173. package/dist/db/migrations/20260501133000_lobu_memory_mcp_id.sql +117 -0
  174. package/dist/db/migrations/20260502000000_drop_chat_connections.sql +60 -0
  175. package/dist/db/migrations/20260503000000_agent_secrets_org_scope.sql +56 -0
  176. package/dist/db/migrations/20260504000000_flatten_agents_drop_sandbox_model.sql +48 -0
  177. package/dist/db/migrations/20260510220000_connector_required_capability.sql +47 -0
  178. package/dist/db/migrations/20260512000000_device_worker_connection_binding.sql +113 -0
  179. package/dist/db/migrations/20260512131703_connections_slug.sql +131 -0
  180. package/dist/db/migrations/20260513000000_chat_user_identities.sql +24 -0
  181. package/dist/db/migrations/20260513120000_auth_profiles_device_binding.sql +50 -0
  182. package/dist/db/migrations/20260513150000_auth_profiles_cdp_url.sql +43 -0
  183. package/dist/db/migrations/20260513200000_notifications_as_events.sql +86 -0
  184. package/dist/db/migrations/20260514000000_scheduled_jobs.sql +97 -0
  185. package/dist/db/migrations/20260514120000_auth_profiles_connector_key_nullable.sql +42 -0
  186. package/dist/eval/types.d.ts +2 -0
  187. package/dist/eval/types.d.ts.map +1 -1
  188. package/dist/index.d.ts +11 -0
  189. package/dist/index.d.ts.map +1 -1
  190. package/dist/index.js +210 -132
  191. package/dist/index.js.map +1 -1
  192. package/dist/internal/api-client.d.ts +4 -8
  193. package/dist/internal/api-client.d.ts.map +1 -1
  194. package/dist/internal/api-client.js +1 -1
  195. package/dist/internal/api-client.js.map +1 -1
  196. package/dist/internal/context.js +2 -2
  197. package/dist/internal/context.js.map +1 -1
  198. package/dist/internal/credentials.d.ts.map +1 -1
  199. package/dist/internal/credentials.js +6 -1
  200. package/dist/internal/credentials.js.map +1 -1
  201. package/dist/internal/gateway-url.d.ts +14 -0
  202. package/dist/internal/gateway-url.d.ts.map +1 -1
  203. package/dist/internal/gateway-url.js +19 -0
  204. package/dist/internal/gateway-url.js.map +1 -1
  205. package/dist/internal/index.d.ts +3 -4
  206. package/dist/internal/index.d.ts.map +1 -1
  207. package/dist/internal/index.js +3 -3
  208. package/dist/internal/index.js.map +1 -1
  209. package/dist/internal/oauth.d.ts +6 -5
  210. package/dist/internal/oauth.d.ts.map +1 -1
  211. package/dist/internal/oauth.js +2 -2
  212. package/dist/internal/project-link.d.ts +10 -0
  213. package/dist/internal/project-link.d.ts.map +1 -0
  214. package/dist/internal/project-link.js +48 -0
  215. package/dist/internal/project-link.js.map +1 -0
  216. package/dist/providers.json +2 -2
  217. package/dist/server.bundle.mjs +31654 -30866
  218. package/dist/start-local.bundle.mjs +74409 -0
  219. package/dist/templates/README.md.tmpl +10 -11
  220. package/dist/templates/TESTING.md.tmpl +9 -9
  221. package/package.json +15 -13
  222. package/dist/__tests__/chat.integration.test.d.ts +0 -2
  223. package/dist/__tests__/chat.integration.test.d.ts.map +0 -1
  224. package/dist/__tests__/chat.integration.test.js +0 -337
  225. package/dist/__tests__/chat.integration.test.js.map +0 -1
  226. package/dist/__tests__/dev.test.d.ts +0 -2
  227. package/dist/__tests__/dev.test.d.ts.map +0 -1
  228. package/dist/__tests__/dev.test.js +0 -25
  229. package/dist/__tests__/dev.test.js.map +0 -1
  230. package/dist/__tests__/init-memory.test.d.ts +0 -2
  231. package/dist/__tests__/init-memory.test.d.ts.map +0 -1
  232. package/dist/__tests__/init-memory.test.js +0 -45
  233. package/dist/__tests__/init-memory.test.js.map +0 -1
  234. package/dist/__tests__/token.test.d.ts +0 -2
  235. package/dist/__tests__/token.test.d.ts.map +0 -1
  236. package/dist/__tests__/token.test.js +0 -52
  237. package/dist/__tests__/token.test.js.map +0 -1
  238. package/dist/commands/_lib/apply/__tests__/client.test.d.ts +0 -2
  239. package/dist/commands/_lib/apply/__tests__/client.test.d.ts.map +0 -1
  240. package/dist/commands/_lib/apply/__tests__/client.test.js +0 -23
  241. package/dist/commands/_lib/apply/__tests__/client.test.js.map +0 -1
  242. package/dist/commands/_lib/apply/__tests__/desired-state.test.d.ts +0 -2
  243. package/dist/commands/_lib/apply/__tests__/desired-state.test.d.ts.map +0 -1
  244. package/dist/commands/_lib/apply/__tests__/desired-state.test.js +0 -140
  245. package/dist/commands/_lib/apply/__tests__/desired-state.test.js.map +0 -1
  246. package/dist/commands/_lib/apply/__tests__/diff.test.d.ts +0 -2
  247. package/dist/commands/_lib/apply/__tests__/diff.test.d.ts.map +0 -1
  248. package/dist/commands/_lib/apply/__tests__/diff.test.js +0 -378
  249. package/dist/commands/_lib/apply/__tests__/diff.test.js.map +0 -1
  250. package/dist/commands/apply.d.ts +0 -3
  251. package/dist/commands/apply.d.ts.map +0 -1
  252. package/dist/commands/apply.js +0 -5
  253. package/dist/commands/apply.js.map +0 -1
  254. package/dist/commands/memory/_lib/openclaw-auth.test.d.ts +0 -2
  255. package/dist/commands/memory/_lib/openclaw-auth.test.d.ts.map +0 -1
  256. package/dist/commands/memory/_lib/openclaw-auth.test.js +0 -9
  257. package/dist/commands/memory/_lib/openclaw-auth.test.js.map +0 -1
  258. package/dist/internal/__tests__/api-client.test.d.ts +0 -2
  259. package/dist/internal/__tests__/api-client.test.d.ts.map +0 -1
  260. package/dist/internal/__tests__/api-client.test.js +0 -95
  261. package/dist/internal/__tests__/api-client.test.js.map +0 -1
  262. package/dist/internal/__tests__/context.test.d.ts +0 -2
  263. package/dist/internal/__tests__/context.test.d.ts.map +0 -1
  264. package/dist/internal/__tests__/context.test.js +0 -77
  265. package/dist/internal/__tests__/context.test.js.map +0 -1
@@ -1,8 +1,35 @@
1
1
  import { readdir, readFile, stat } from "node:fs/promises";
2
2
  import { join, resolve } from "node:path";
3
+ import Ajv from "ajv";
4
+ import addFormats from "ajv-formats";
3
5
  import { parse as parseToml } from "smol-toml";
4
6
  import { ValidationError } from "../../memory/_lib/errors.js";
7
+ import { expandModelDefinition, parseModelYamlFile, validateModel, } from "../../memory/_lib/schema.js";
5
8
  import { CONFIG_FILENAME, isLoadError, loadConfig, } from "../../../config/loader.js";
9
+ import { CronExpressionParser } from "cron-parser";
10
+ // ── Connector slug / schedule validators (round-2) ─────────────────────────
11
+ // Mirror packages/server/src/utils/connections.ts CONNECTION_SLUG_PATTERN and
12
+ // the server's validateSchedule (packages/server/src/utils/cron.ts) so the CLI
13
+ // fails loud *before* any mutation instead of getting a server 4xx.
14
+ const CONNECTION_SLUG_PATTERN = /^[a-z0-9][a-z0-9-]{0,62}$/;
15
+ // auth_profiles slugs are sanitized server-side; require canonical form so the
16
+ // diff key matches what is stored (server cap is 80 chars).
17
+ const AUTH_PROFILE_SLUG_PATTERN = /^[a-z0-9][a-z0-9-]{0,79}$/;
18
+ const MIN_CRON_INTERVAL_MS = 60_000;
19
+ function cronError(schedule) {
20
+ try {
21
+ const it = CronExpressionParser.parse(schedule);
22
+ const first = it.next().toDate();
23
+ const second = it.next().toDate();
24
+ if (second.getTime() - first.getTime() < MIN_CRON_INTERVAL_MS) {
25
+ return `schedule "${schedule}" is too frequent (minimum interval is 1 minute)`;
26
+ }
27
+ return null;
28
+ }
29
+ catch (err) {
30
+ return `invalid cron expression "${schedule}" — ${err instanceof Error ? err.message : String(err)}`;
31
+ }
32
+ }
6
33
  // ── Stable platform IDs (mirror of file-loader.ts) ─────────────────────────
7
34
  //
8
35
  // keep in sync with packages/server/src/gateway/config/file-loader.ts
@@ -25,55 +52,48 @@ function asEnvRef(value) {
25
52
  const match = ENV_REF.exec(value.trim());
26
53
  return match?.[1] ?? null;
27
54
  }
55
+ /** Visit every string leaf in `value` (recursing arrays + plain objects). */
56
+ function walkStrings(value, visit) {
57
+ if (typeof value === "string") {
58
+ visit(value);
59
+ return;
60
+ }
61
+ if (Array.isArray(value)) {
62
+ for (const item of value)
63
+ walkStrings(item, visit);
64
+ return;
65
+ }
66
+ if (value && typeof value === "object") {
67
+ for (const v of Object.values(value)) {
68
+ walkStrings(v, visit);
69
+ }
70
+ }
71
+ }
72
+ /** Add every `$ENV` reference found among the string leaves of `value`. */
73
+ function collectEnvRefsFrom(value, out) {
74
+ walkStrings(value, (s) => {
75
+ const ref = asEnvRef(s);
76
+ if (ref)
77
+ out.add(ref);
78
+ });
79
+ }
28
80
  function collectEnvRefs(config, out) {
29
81
  for (const agentConfig of Object.values(config.agents)) {
30
82
  for (const provider of agentConfig.providers) {
31
- if (provider.key) {
32
- const ref = asEnvRef(provider.key);
33
- if (ref)
34
- out.add(ref);
35
- }
36
- if (provider.secret_ref) {
37
- const ref = asEnvRef(provider.secret_ref);
38
- if (ref)
39
- out.add(ref);
40
- }
83
+ if (provider.key)
84
+ collectEnvRefsFrom(provider.key, out);
85
+ if (provider.secret_ref)
86
+ collectEnvRefsFrom(provider.secret_ref, out);
41
87
  }
42
88
  for (const platform of agentConfig.platforms) {
43
- for (const value of Object.values(platform.config)) {
44
- const ref = asEnvRef(value);
45
- if (ref)
46
- out.add(ref);
47
- }
89
+ collectEnvRefsFrom(platform.config, out);
48
90
  }
49
91
  if (agentConfig.skills.mcp) {
50
92
  for (const mcp of Object.values(agentConfig.skills.mcp)) {
51
- if (mcp.headers) {
52
- for (const v of Object.values(mcp.headers)) {
53
- const ref = asEnvRef(v);
54
- if (ref)
55
- out.add(ref);
56
- }
57
- }
58
- if (mcp.env) {
59
- for (const v of Object.values(mcp.env)) {
60
- const ref = asEnvRef(v);
61
- if (ref)
62
- out.add(ref);
63
- }
64
- }
65
- if (mcp.oauth) {
66
- if (mcp.oauth.client_id) {
67
- const ref = asEnvRef(mcp.oauth.client_id);
68
- if (ref)
69
- out.add(ref);
70
- }
71
- if (mcp.oauth.client_secret) {
72
- const ref = asEnvRef(mcp.oauth.client_secret);
73
- if (ref)
74
- out.add(ref);
75
- }
76
- }
93
+ collectEnvRefsFrom(mcp.headers, out);
94
+ collectEnvRefsFrom(mcp.env, out);
95
+ collectEnvRefsFrom(mcp.oauth?.client_id, out);
96
+ collectEnvRefsFrom(mcp.oauth?.client_secret, out);
77
97
  }
78
98
  }
79
99
  }
@@ -426,6 +446,17 @@ function buildPlatforms(agentId, agentConfig, env) {
426
446
  };
427
447
  if (platform.name)
428
448
  desired.name = platform.name;
449
+ if (platform.channels && platform.channels.length > 0) {
450
+ if (platform.type !== "slack") {
451
+ throw new ValidationError(`agent "${agentId}" platform "${platform.type}": \`channels\` is only supported for Slack`);
452
+ }
453
+ for (const entry of platform.channels) {
454
+ if (!/^[^/\s]+\/[^/\s]+$/.test(entry.trim())) {
455
+ throw new ValidationError(`agent "${agentId}" Slack \`channels\` entry "${entry}" must be in "<teamId>/<channelId>" form (e.g. "T0ABCDEF/C0123ABCD")`);
456
+ }
457
+ }
458
+ desired.channels = platform.channels.map((e) => e.trim());
459
+ }
429
460
  out.push(desired);
430
461
  }
431
462
  return out;
@@ -435,25 +466,61 @@ function isRecord(value) {
435
466
  }
436
467
  function parseEntityType(raw) {
437
468
  if (!isRecord(raw) || typeof raw.slug !== "string") {
438
- throw new ValidationError(`memory.entity_types entries must be objects with a "slug" string field; got ${JSON.stringify(raw)}`);
469
+ throw new ValidationError(`model-bundle "entities" entries must be objects with a "slug" string field; got ${JSON.stringify(raw)}`);
439
470
  }
440
471
  const out = { slug: raw.slug };
441
472
  if (typeof raw.name === "string")
442
473
  out.name = raw.name;
443
474
  if (typeof raw.description === "string")
444
475
  out.description = raw.description;
445
- if (Array.isArray(raw.required)) {
446
- out.required = raw.required.filter((v) => typeof v === "string");
476
+ if (isRecord(raw.metadata_schema)) {
477
+ if (Array.isArray(raw.metadata_schema.required)) {
478
+ out.required = raw.metadata_schema.required.filter((v) => typeof v === "string");
479
+ }
480
+ if (isRecord(raw.metadata_schema.properties)) {
481
+ out.properties = raw.metadata_schema.properties;
482
+ }
447
483
  }
448
- if (isRecord(raw.properties))
449
- out.properties = raw.properties;
450
484
  if (isRecord(raw.metadata))
451
485
  out.metadata = raw.metadata;
452
486
  return out;
453
487
  }
488
+ function parseWatcher(raw) {
489
+ if (!isRecord(raw) || typeof raw.slug !== "string") {
490
+ throw new ValidationError(`watcher model files must be objects with a "slug" string field; got ${JSON.stringify(raw)}`);
491
+ }
492
+ if (typeof raw.prompt !== "string" || !raw.prompt.trim()) {
493
+ throw new ValidationError(`watcher "${raw.slug}" is missing a "prompt" string`);
494
+ }
495
+ if (typeof raw.agent !== "string" || !raw.agent.trim()) {
496
+ throw new ValidationError(`watcher "${raw.slug}" is missing an "agent" field — every watcher must name the agent that owns it (e.g. \`agent: my-agent\`, matching an \`[agents.<id>]\` block in lobu.toml)`);
497
+ }
498
+ const extractionSchema = isRecord(raw.extraction_schema)
499
+ ? raw.extraction_schema
500
+ : {};
501
+ const out = {
502
+ slug: raw.slug,
503
+ agent: raw.agent,
504
+ prompt: raw.prompt,
505
+ extractionSchema,
506
+ };
507
+ if (typeof raw.name === "string")
508
+ out.name = raw.name;
509
+ if (typeof raw.description === "string")
510
+ out.description = raw.description;
511
+ if (typeof raw.schedule === "string")
512
+ out.schedule = raw.schedule;
513
+ if (Array.isArray(raw.sources)) {
514
+ out.sources = raw.sources
515
+ .filter(isRecord)
516
+ .filter((s) => typeof s.name === "string" && typeof s.query === "string")
517
+ .map((s) => ({ name: s.name, query: s.query }));
518
+ }
519
+ return out;
520
+ }
454
521
  function parseRelationshipType(raw) {
455
522
  if (!isRecord(raw) || typeof raw.slug !== "string") {
456
- throw new ValidationError(`memory.relationship_types entries must be objects with a "slug" string field; got ${JSON.stringify(raw)}`);
523
+ throw new ValidationError(`model-bundle "relationships" entries must be objects with a "slug" string field; got ${JSON.stringify(raw)}`);
457
524
  }
458
525
  const out = { slug: raw.slug };
459
526
  if (typeof raw.name === "string")
@@ -471,57 +538,78 @@ function parseRelationshipType(raw) {
471
538
  return out;
472
539
  }
473
540
  /**
474
- * Read memory schema files referenced by `[memory.owletto].models`. Each YAML
475
- * file in that directory should declare `type: entity_type` or
476
- * `type: relationship_type` (matches the seed-cmd schema).
477
- *
478
- * v1: parse only entity_type and relationship_type. Watchers are deferred.
541
+ * Read memory schema files referenced by `[memory].models`. Every nested YAML
542
+ * file must be a dbt-style `version: 2` bundle with top-level `entities`,
543
+ * `relationships`, and `watchers` arrays; multi-document YAML streams are
544
+ * supported. `lobu apply` syncs entity types, relationship types, and watchers
545
+ * from these files; watcher sync is create-only (drift ignored).
479
546
  */
480
- async function loadMemorySchema(config, projectRoot) {
481
- const empty = { entityTypes: [], relationshipTypes: [] };
482
- const owletto = config.memory?.owletto;
483
- if (!owletto || owletto.enabled === false)
547
+ async function loadMemoryModels(config, projectRoot) {
548
+ const empty = {
549
+ entityTypes: [],
550
+ relationshipTypes: [],
551
+ watchers: [],
552
+ };
553
+ const mem = config.memory;
554
+ if (!mem || mem.enabled === false)
484
555
  return empty;
485
- const inline = config.memory;
486
- if (inline?.schema) {
487
- const entityTypesRaw = Array.isArray(inline.schema.entity_types)
488
- ? inline.schema.entity_types
489
- : [];
490
- const relTypesRaw = Array.isArray(inline.schema.relationship_types)
491
- ? inline.schema.relationship_types
492
- : [];
493
- return {
494
- entityTypes: entityTypesRaw.map(parseEntityType),
495
- relationshipTypes: relTypesRaw.map(parseRelationshipType),
496
- };
497
- }
498
556
  // Models directory (matches seed-cmd's resolution rules).
499
- const modelsRel = owletto.models?.trim() || "./models";
557
+ const modelsRel = mem.models?.trim() || "./models";
500
558
  const modelsPath = resolve(projectRoot, modelsRel);
501
559
  const { existsSync, readdirSync, readFileSync } = await import("node:fs");
502
- const { parse: parseYaml } = await import("yaml");
503
560
  if (!existsSync(modelsPath))
504
561
  return empty;
505
562
  const entityTypes = [];
506
563
  const relationshipTypes = [];
507
- const files = readdirSync(modelsPath)
508
- .filter((f) => f.endsWith(".yaml") || f.endsWith(".yml"))
509
- .sort();
510
- for (const file of files) {
564
+ const watchers = [];
565
+ const readModelFiles = (dir, prefix = "") => {
566
+ return readdirSync(dir, { withFileTypes: true })
567
+ .sort((a, b) => a.name.localeCompare(b.name))
568
+ .flatMap((entry) => {
569
+ const relPath = prefix ? join(prefix, entry.name) : entry.name;
570
+ const fullPath = join(dir, entry.name);
571
+ if (entry.isDirectory())
572
+ return readModelFiles(fullPath, relPath);
573
+ if (entry.isFile() &&
574
+ (entry.name.endsWith(".yaml") || entry.name.endsWith(".yml"))) {
575
+ return [relPath];
576
+ }
577
+ return [];
578
+ });
579
+ };
580
+ const errors = [];
581
+ for (const file of readModelFiles(modelsPath)) {
511
582
  const raw = readFileSync(join(modelsPath, file), "utf-8");
512
- const parsed = parseYaml(raw);
513
- if (!isRecord(parsed) || typeof parsed.type !== "string")
514
- continue;
515
- if (parsed.type === "entity_type" || parsed.type === "entity") {
516
- entityTypes.push(parseEntityType(parsed));
517
- }
518
- else if (parsed.type === "relationship_type" ||
519
- parsed.type === "relationship") {
520
- relationshipTypes.push(parseRelationshipType(parsed));
583
+ const { documents, errors: parseErrors } = parseModelYamlFile(raw, file);
584
+ errors.push(...parseErrors);
585
+ for (const { data: document, file: documentFile } of documents) {
586
+ const expanded = expandModelDefinition(document, documentFile);
587
+ errors.push(...expanded.errors);
588
+ for (const model of expanded.models) {
589
+ const modelErrors = validateModel(model.data, model.file);
590
+ if (modelErrors.length > 0) {
591
+ errors.push(...modelErrors);
592
+ continue;
593
+ }
594
+ if (model.modelType === "entity") {
595
+ entityTypes.push(parseEntityType(model.data));
596
+ }
597
+ else if (model.modelType === "relationship") {
598
+ relationshipTypes.push(parseRelationshipType(model.data));
599
+ }
600
+ else if (model.modelType === "watcher") {
601
+ watchers.push(parseWatcher(model.data));
602
+ }
603
+ }
521
604
  }
522
- // watcher files are out of scope for v1 apply
523
605
  }
524
- return { entityTypes, relationshipTypes };
606
+ if (errors.length > 0) {
607
+ const detail = errors
608
+ .map((e) => `${e.file}: ${e.field} — ${e.message}`)
609
+ .join("\n ");
610
+ throw new ValidationError(`Model validation failed\n ${detail}`);
611
+ }
612
+ return { entityTypes, relationshipTypes, watchers };
525
613
  }
526
614
  /**
527
615
  * The Zod schema strips unknown keys, so we re-parse the raw TOML to surface
@@ -552,10 +640,513 @@ async function rejectUnsupportedAgentShapes(cwd) {
552
640
  continue;
553
641
  const watchers = agentConfig.watchers;
554
642
  if (Array.isArray(watchers) && watchers.length > 0) {
555
- throw new ValidationError(`agent "${agentId}" declares [[agents.${agentId}.watchers]] \`lobu apply\` does not sync watchers in v1. Remove the block or use \`lobu memory seed\`.`);
643
+ throw new ValidationError(`agent "${agentId}" declares [[agents.${agentId}.watchers]] in lobu.toml watchers live in a \`[memory].models\` YAML bundle (the same file as your entity types), each with an \`agent: ${agentId}\` field pointing back here. Move the watcher there.`);
556
644
  }
557
645
  }
558
646
  }
647
+ // ── Connectors (data-source connectors) ───────────────────────────────────
648
+ const AUTH_PROFILE_KINDS = new Set([
649
+ "env",
650
+ "oauth_app",
651
+ "oauth_account",
652
+ "browser_session",
653
+ ]);
654
+ function asString(value) {
655
+ return typeof value === "string" && value.trim() ? value : undefined;
656
+ }
657
+ function parseConnectionDoc(raw, file) {
658
+ const slug = asString(raw.slug);
659
+ if (!slug) {
660
+ throw new ValidationError(`${file}: \`type: connection\` doc is missing a "slug" string`);
661
+ }
662
+ if (!CONNECTION_SLUG_PATTERN.test(slug)) {
663
+ throw new ValidationError(`${file}: connection slug "${slug}" must match /^[a-z0-9][a-z0-9-]{0,62}$/ (lowercase letters/digits/hyphens, no leading hyphen, ≤63 chars)`);
664
+ }
665
+ const connector = asString(raw.connector);
666
+ if (!connector) {
667
+ throw new ValidationError(`${file}: connection "${slug}" is missing a "connector" key`);
668
+ }
669
+ const out = {
670
+ slug,
671
+ connector,
672
+ feeds: [],
673
+ sourceFile: file,
674
+ };
675
+ const name = asString(raw.name);
676
+ if (name)
677
+ out.name = name;
678
+ const auth = asString(raw.auth);
679
+ if (auth)
680
+ out.authProfileSlug = auth;
681
+ const appAuth = asString(raw.app_auth);
682
+ if (appAuth)
683
+ out.appAuthProfileSlug = appAuth;
684
+ if (raw.config !== undefined) {
685
+ if (!isRecord(raw.config)) {
686
+ throw new ValidationError(`${file}: connection "${slug}" \`config\` must be an object`);
687
+ }
688
+ out.config = raw.config;
689
+ }
690
+ if (raw.feeds !== undefined) {
691
+ if (!Array.isArray(raw.feeds)) {
692
+ throw new ValidationError(`${file}: connection "${slug}" \`feeds\` must be an array`);
693
+ }
694
+ const seen = new Set();
695
+ for (const entry of raw.feeds) {
696
+ if (!isRecord(entry)) {
697
+ throw new ValidationError(`${file}: connection "${slug}" feed entries must be objects`);
698
+ }
699
+ const feedKey = asString(entry.feed);
700
+ if (!feedKey) {
701
+ throw new ValidationError(`${file}: connection "${slug}" feed entry is missing a "feed" key`);
702
+ }
703
+ if (seen.has(feedKey)) {
704
+ throw new ValidationError(`${file}: connection "${slug}" declares feed "${feedKey}" twice`);
705
+ }
706
+ seen.add(feedKey);
707
+ const feed = { feedKey };
708
+ const feedName = asString(entry.name);
709
+ if (feedName)
710
+ feed.name = feedName;
711
+ const schedule = asString(entry.schedule);
712
+ if (schedule) {
713
+ const err = cronError(schedule);
714
+ if (err) {
715
+ throw new ValidationError(`${file}: connection "${slug}" feed "${feedKey}" ${err}`);
716
+ }
717
+ feed.schedule = schedule;
718
+ }
719
+ if (entry.config !== undefined) {
720
+ if (!isRecord(entry.config)) {
721
+ throw new ValidationError(`${file}: connection "${slug}" feed "${feedKey}" \`config\` must be an object`);
722
+ }
723
+ feed.config = entry.config;
724
+ }
725
+ out.feeds.push(feed);
726
+ }
727
+ }
728
+ return out;
729
+ }
730
+ function parseAuthProfileDoc(raw, file) {
731
+ const slug = asString(raw.slug);
732
+ if (!slug) {
733
+ throw new ValidationError(`${file}: \`type: auth_profile\` doc is missing a "slug" string`);
734
+ }
735
+ if (!AUTH_PROFILE_SLUG_PATTERN.test(slug)) {
736
+ throw new ValidationError(`${file}: auth_profile slug "${slug}" must match /^[a-z0-9][a-z0-9-]{0,79}$/ (lowercase letters/digits/hyphens, no leading hyphen, ≤80 chars)`);
737
+ }
738
+ const connector = asString(raw.connector);
739
+ if (!connector) {
740
+ throw new ValidationError(`${file}: auth_profile "${slug}" is missing a "connector" key`);
741
+ }
742
+ const kind = asString(raw.kind);
743
+ if (!kind || !AUTH_PROFILE_KINDS.has(kind)) {
744
+ throw new ValidationError(`${file}: auth_profile "${slug}" \`kind\` must be one of env|oauth_app|oauth_account|browser_session (got ${JSON.stringify(raw.kind)})`);
745
+ }
746
+ const out = {
747
+ slug,
748
+ connector,
749
+ kind: kind,
750
+ sourceFile: file,
751
+ };
752
+ const name = asString(raw.name);
753
+ if (name)
754
+ out.name = name;
755
+ if (raw.credentials !== undefined) {
756
+ if (!isRecord(raw.credentials)) {
757
+ throw new ValidationError(`${file}: auth_profile "${slug}" \`credentials\` must be an object`);
758
+ }
759
+ const creds = {};
760
+ for (const [k, v] of Object.entries(raw.credentials)) {
761
+ if (typeof v !== "string") {
762
+ throw new ValidationError(`${file}: auth_profile "${slug}" credential "${k}" must be a string (use $ENV for secrets)`);
763
+ }
764
+ creds[k] = v;
765
+ }
766
+ if (kind === "oauth_account" || kind === "browser_session") {
767
+ if (Object.keys(creds).length > 0) {
768
+ throw new ValidationError(`${file}: auth_profile "${slug}" has \`kind: ${kind}\` — credentials must not be set; \`lobu apply\` never writes interactive-auth tokens (complete auth via the connect URL).`);
769
+ }
770
+ }
771
+ else {
772
+ out.credentials = creds;
773
+ }
774
+ }
775
+ return out;
776
+ }
777
+ function parseConnectorDoc(raw, file) {
778
+ const key = asString(raw.key);
779
+ if (!key) {
780
+ throw new ValidationError(`${file}: \`type: connector\` doc is missing a "key" string`);
781
+ }
782
+ const sourcePath = asString(raw.source_path);
783
+ const sourceUrl = asString(raw.source_url);
784
+ if (!!sourcePath === !!sourceUrl) {
785
+ throw new ValidationError(`${file}: connector "${key}" must declare exactly one of \`source_path\` or \`source_url\``);
786
+ }
787
+ if (sourceUrl) {
788
+ let parsed;
789
+ try {
790
+ parsed = new URL(sourceUrl);
791
+ }
792
+ catch {
793
+ throw new ValidationError(`${file}: connector "${key}" source_url is not a valid URL: ${sourceUrl}`);
794
+ }
795
+ if (parsed.protocol !== "https:") {
796
+ throw new ValidationError(`${file}: connector "${key}" source_url must use https (got ${parsed.protocol}//)`);
797
+ }
798
+ }
799
+ return {
800
+ key,
801
+ ...(sourcePath ? { sourcePath } : {}),
802
+ ...(sourceUrl ? { sourceUrl } : {}),
803
+ };
804
+ }
805
+ const EMPTY_CONNECTORS = {
806
+ definitions: [],
807
+ authProfiles: [],
808
+ connections: [],
809
+ };
810
+ /**
811
+ * Load the `[memory].connectors` directory:
812
+ * - every `*.connector.ts` is auto-discovered as a connector definition
813
+ * (raw source pushed to the server, which compiles + extracts the key)
814
+ * - `*.yaml` files are multi-doc (`---`-separated); each doc carries
815
+ * `version: 1` and a `type:` of `connection`, `auth_profile`, or `connector`
816
+ *
817
+ * `connector:` config validation against the connector's `optionsSchema` /
818
+ * feed `configSchema` / `authSchema` happens later (in `apply-cmd`) once the
819
+ * remote connector-definition catalog is available — the CLI never compiles
820
+ * connectors locally.
821
+ */
822
+ async function loadConnectors(config, projectRoot, env, envRefs) {
823
+ const mem = config.memory;
824
+ if (!mem || mem.enabled === false)
825
+ return EMPTY_CONNECTORS;
826
+ const dirRel = mem.connectors?.trim() || "./connectors";
827
+ const dirPath = resolve(projectRoot, dirRel);
828
+ let entries;
829
+ try {
830
+ entries = (await readdir(dirPath)).sort();
831
+ }
832
+ catch {
833
+ return EMPTY_CONNECTORS;
834
+ }
835
+ const { parseAllDocuments } = await import("yaml");
836
+ const definitionsByKey = new Map();
837
+ // Keys explicitly declared by a `type: connector` doc (vs auto-discovered
838
+ // from a `*.connector.ts` filename). A given connector key may be declared by
839
+ // at most one such doc — even two docs pointing at the same `source_path`.
840
+ const connectorDocKeyDeclaredBy = new Map();
841
+ // `.connector.ts` files keyed by their *absolute path* — we don't know the
842
+ // connector key until the server compiles them. `type: connector` docs with
843
+ // `source_path:` that point at one of these files just dedupe to the file.
844
+ const tsFileDefinitions = new Map();
845
+ const authProfiles = new Map();
846
+ const connections = new Map();
847
+ for (const entry of entries) {
848
+ const entryPath = join(dirPath, entry);
849
+ let entryStat;
850
+ try {
851
+ entryStat = await stat(entryPath);
852
+ }
853
+ catch {
854
+ continue;
855
+ }
856
+ if (!entryStat.isFile())
857
+ continue;
858
+ // Auto-discovered local connector definition.
859
+ if (entry.endsWith(".connector.ts")) {
860
+ const sourceCode = await readFile(entryPath, "utf-8");
861
+ tsFileDefinitions.set(entryPath, {
862
+ key: null,
863
+ sourcePath: entryPath,
864
+ sourceCode,
865
+ sourceFile: `${dirRel}/${entry}`,
866
+ });
867
+ continue;
868
+ }
869
+ if (!entry.endsWith(".yaml") && !entry.endsWith(".yml"))
870
+ continue;
871
+ const rel = `${dirRel}/${entry}`;
872
+ const raw = await readFile(entryPath, "utf-8");
873
+ let docs;
874
+ try {
875
+ docs = parseAllDocuments(raw)
876
+ .map((doc) => doc.toJSON())
877
+ .filter((doc) => doc !== null && doc !== undefined);
878
+ }
879
+ catch (err) {
880
+ throw new ValidationError(`${rel}: failed to parse YAML — ${err instanceof Error ? err.message : String(err)}`);
881
+ }
882
+ for (const doc of docs) {
883
+ if (!isRecord(doc)) {
884
+ throw new ValidationError(`${rel}: each connectors doc must be a mapping with \`version\` and \`type\``);
885
+ }
886
+ const type = asString(doc.type);
887
+ if (!type) {
888
+ throw new ValidationError(`${rel}: connectors doc is missing a "type" (connection|auth_profile|connector)`);
889
+ }
890
+ if (doc.version !== undefined && doc.version !== 1) {
891
+ throw new ValidationError(`${rel}: unsupported connectors doc version ${JSON.stringify(doc.version)} (expected 1)`);
892
+ }
893
+ if (type === "connection") {
894
+ const conn = parseConnectionDoc(doc, rel);
895
+ if (connections.has(conn.slug)) {
896
+ throw new ValidationError(`${rel}: duplicate connection slug "${conn.slug}"`);
897
+ }
898
+ connections.set(conn.slug, conn);
899
+ }
900
+ else if (type === "auth_profile") {
901
+ const profile = parseAuthProfileDoc(doc, rel);
902
+ if (authProfiles.has(profile.slug)) {
903
+ throw new ValidationError(`${rel}: duplicate auth_profile slug "${profile.slug}"`);
904
+ }
905
+ if (profile.credentials) {
906
+ // Expand `$ENV` refs in-place (collect them too, so the apply
907
+ // secrets gate fails loud) — never push literal `$NAME` strings.
908
+ const resolved = {};
909
+ for (const [k, v] of Object.entries(profile.credentials)) {
910
+ const ref = asEnvRef(v);
911
+ if (!ref) {
912
+ resolved[k] = v;
913
+ continue;
914
+ }
915
+ envRefs.add(ref);
916
+ const value = env[ref];
917
+ if (value === undefined || value === "") {
918
+ throw new ValidationError(`${rel}: auth_profile "${profile.slug}" credential "${k}" references $${ref}, but it is unset or empty in the apply environment`);
919
+ }
920
+ resolved[k] = value;
921
+ }
922
+ profile.credentials = resolved;
923
+ }
924
+ authProfiles.set(profile.slug, profile);
925
+ }
926
+ else if (type === "connector") {
927
+ const parsed = parseConnectorDoc(doc, rel);
928
+ const priorDoc = connectorDocKeyDeclaredBy.get(parsed.key);
929
+ if (priorDoc) {
930
+ throw new ValidationError(`connector key "${parsed.key}" is declared by two \`type: connector\` docs — ${priorDoc} and ${rel}; keys must be unique`);
931
+ }
932
+ connectorDocKeyDeclaredBy.set(parsed.key, rel);
933
+ if (parsed.sourceUrl) {
934
+ const prior = definitionsByKey.get(parsed.key);
935
+ if (prior) {
936
+ throw new ValidationError(`connector key "${parsed.key}" is declared twice — in ${prior.sourceFile} and ${rel}; keys must be unique`);
937
+ }
938
+ const priorTs = [...tsFileDefinitions.values()].find((d) => d.key === parsed.key);
939
+ if (priorTs) {
940
+ throw new ValidationError(`connector key "${parsed.key}" is declared twice — in ${priorTs.sourceFile} and ${rel}; keys must be unique`);
941
+ }
942
+ definitionsByKey.set(parsed.key, {
943
+ key: parsed.key,
944
+ sourceUrl: parsed.sourceUrl,
945
+ sourceFile: rel,
946
+ });
947
+ }
948
+ else if (parsed.sourcePath) {
949
+ // `source_path` is resolved relative to the manifest YAML file's
950
+ // directory (the connectors/ dir), matching the watcher-classifier
951
+ // `source_path` convention.
952
+ const abs = resolve(dirPath, parsed.sourcePath);
953
+ // The declared key must not collide with another connector definition.
954
+ const keyClash = definitionsByKey.get(parsed.key) ??
955
+ [...tsFileDefinitions.entries()].find(([p, d]) => d.key === parsed.key && p !== abs)?.[1];
956
+ if (keyClash) {
957
+ throw new ValidationError(`connector key "${parsed.key}" is declared twice — in ${keyClash.sourceFile} and ${rel}; keys must be unique`);
958
+ }
959
+ if (tsFileDefinitions.has(abs)) {
960
+ // Already auto-discovered as a `*.connector.ts` file; the
961
+ // `type: connector` doc just declares its key for clearer output.
962
+ const existing = tsFileDefinitions.get(abs);
963
+ if (existing) {
964
+ if (existing.key !== null && existing.key !== parsed.key) {
965
+ throw new ValidationError(`${existing.sourceFile} declares connector key "${existing.key}" but ${rel} declares "${parsed.key}" for the same file — they must agree`);
966
+ }
967
+ existing.key = parsed.key;
968
+ }
969
+ }
970
+ else {
971
+ let sourceCode;
972
+ try {
973
+ sourceCode = await readFile(abs, "utf-8");
974
+ }
975
+ catch {
976
+ throw new ValidationError(`${rel}: connector "${parsed.key}" \`source_path\` ${parsed.sourcePath} does not exist`);
977
+ }
978
+ tsFileDefinitions.set(abs, {
979
+ key: parsed.key,
980
+ sourcePath: abs,
981
+ sourceCode,
982
+ sourceFile: rel,
983
+ });
984
+ }
985
+ }
986
+ }
987
+ else {
988
+ throw new ValidationError(`${rel}: unknown connectors doc type "${type}" (expected connection|auth_profile|connector)`);
989
+ }
990
+ }
991
+ }
992
+ const allDefs = [...definitionsByKey.values(), ...tsFileDefinitions.values()];
993
+ const seenKeys = new Map();
994
+ for (const def of allDefs) {
995
+ if (def.key === null)
996
+ continue;
997
+ const prior = seenKeys.get(def.key);
998
+ if (prior) {
999
+ throw new ValidationError(`connector key "${def.key}" is declared twice — in ${prior} and ${def.sourceFile}; keys must be unique`);
1000
+ }
1001
+ seenKeys.set(def.key, def.sourceFile);
1002
+ }
1003
+ return {
1004
+ definitions: allDefs.sort((a, b) => (a.key ?? a.sourceFile).localeCompare(b.key ?? b.sourceFile)),
1005
+ authProfiles: [...authProfiles.values()].sort((a, b) => a.slug.localeCompare(b.slug)),
1006
+ connections: [...connections.values()].sort((a, b) => a.slug.localeCompare(b.slug)),
1007
+ };
1008
+ }
1009
+ function schemaFromAuthMethods(authSchema) {
1010
+ const kinds = new Set();
1011
+ if (!authSchema || typeof authSchema !== "object")
1012
+ return kinds;
1013
+ const methods = authSchema.methods;
1014
+ if (!Array.isArray(methods))
1015
+ return kinds;
1016
+ for (const method of methods) {
1017
+ if (!isRecord(method))
1018
+ continue;
1019
+ const t = asString(method.type);
1020
+ // ConnectorAuthMethod `type` ∈ env_keys | oauth | browser | interactive | none
1021
+ if (t === "env_keys")
1022
+ kinds.add("env");
1023
+ else if (t === "oauth") {
1024
+ kinds.add("oauth_app");
1025
+ kinds.add("oauth_account");
1026
+ }
1027
+ else if (t === "browser" || t === "interactive") {
1028
+ kinds.add("browser_session");
1029
+ }
1030
+ }
1031
+ return kinds;
1032
+ }
1033
+ /**
1034
+ * Build per-connector validation schemas from a connector definition. Accepts
1035
+ * either a typed `ConnectorDefinition` (from `@lobu/connector-sdk`) or the
1036
+ * snake_cased shape the server's `manage_connections list_connector_definitions`
1037
+ * returns (`options_schema`, `feeds_schema`, `auth_schema`).
1038
+ */
1039
+ export function resolveConnectorSchemas(def) {
1040
+ const optionsSchema = ("optionsSchema" in def ? def.optionsSchema : undefined) ??
1041
+ ("options_schema" in def ? (def.options_schema ?? undefined) : undefined) ??
1042
+ undefined;
1043
+ const feedsRaw = ("feeds" in def ? def.feeds : undefined) ??
1044
+ ("feeds_schema" in def ? (def.feeds_schema ?? undefined) : undefined) ??
1045
+ undefined;
1046
+ const authSchema = ("authSchema" in def ? def.authSchema : undefined) ??
1047
+ ("auth_schema" in def ? (def.auth_schema ?? undefined) : undefined) ??
1048
+ undefined;
1049
+ const feedKeys = new Set();
1050
+ const feedConfigSchemas = new Map();
1051
+ if (feedsRaw && typeof feedsRaw === "object") {
1052
+ for (const [feedKey, feedDef] of Object.entries(feedsRaw)) {
1053
+ if (!feedDef || typeof feedDef !== "object")
1054
+ continue;
1055
+ feedKeys.add(feedKey);
1056
+ const cfg = feedDef.configSchema;
1057
+ if (cfg && typeof cfg === "object") {
1058
+ feedConfigSchemas.set(feedKey, cfg);
1059
+ }
1060
+ }
1061
+ }
1062
+ return {
1063
+ ...(optionsSchema ? { optionsSchema } : {}),
1064
+ feedKeys,
1065
+ feedConfigSchemas,
1066
+ authKinds: schemaFromAuthMethods(authSchema),
1067
+ };
1068
+ }
1069
+ let sharedAjv = null;
1070
+ function getAjv() {
1071
+ if (!sharedAjv) {
1072
+ sharedAjv = new Ajv({ allErrors: true, strict: false });
1073
+ addFormats(sharedAjv);
1074
+ }
1075
+ return sharedAjv;
1076
+ }
1077
+ function validateAgainstSchema(schema, value, context) {
1078
+ const ajv = getAjv();
1079
+ let validate;
1080
+ try {
1081
+ validate = ajv.compile(schema);
1082
+ }
1083
+ catch (err) {
1084
+ // A malformed connector schema is the connector author's problem, not the
1085
+ // operator's — surface it but don't block the whole apply on it.
1086
+ throw new ValidationError(`${context}: connector declares an invalid JSON schema — ${err instanceof Error ? err.message : String(err)}`);
1087
+ }
1088
+ if (!validate(value ?? {})) {
1089
+ const detail = (validate.errors ?? [])
1090
+ .map((e) => `${e.instancePath || "(root)"} ${e.message ?? ""}`.trim())
1091
+ .join("; ");
1092
+ throw new ValidationError(`${context}: ${detail || "does not match the connector schema"}`);
1093
+ }
1094
+ }
1095
+ /**
1096
+ * Validate a single connection (+ its feeds) and its referenced auth-profile
1097
+ * kinds against a resolved connector schema. Pass `null` to skip schema
1098
+ * checks (e.g. a connector that only exists as a local `.ts` not yet
1099
+ * compiled by the server) — structural checks have already run at load time.
1100
+ */
1101
+ export function validateConnectionAgainstConnector(connection, authProfiles, schemas) {
1102
+ // Validate against `{}` when config is omitted too — that surfaces missing
1103
+ // required keys instead of letting an empty config slip through.
1104
+ if (schemas?.optionsSchema) {
1105
+ validateAgainstSchema(schemas.optionsSchema, connection.config ?? {}, `${connection.sourceFile}: connection "${connection.slug}" config`);
1106
+ }
1107
+ for (const feed of connection.feeds) {
1108
+ if (!schemas)
1109
+ continue;
1110
+ if (schemas.feedKeys.size > 0 && !schemas.feedKeys.has(feed.feedKey)) {
1111
+ throw new ValidationError(`${connection.sourceFile}: connection "${connection.slug}" references unknown feed "${feed.feedKey}" for connector "${connection.connector}" (known feeds: ${[...schemas.feedKeys].sort().join(", ") || "(none)"})`);
1112
+ }
1113
+ const feedSchema = schemas.feedConfigSchemas.get(feed.feedKey);
1114
+ if (feedSchema) {
1115
+ validateAgainstSchema(feedSchema, feed.config ?? {}, `${connection.sourceFile}: connection "${connection.slug}" feed "${feed.feedKey}" config`);
1116
+ }
1117
+ }
1118
+ // `auth:` must reference a runtime/account profile (never `oauth_app`);
1119
+ // `app_auth:` must reference an `oauth_app` profile.
1120
+ if (connection.authProfileSlug) {
1121
+ const profile = requireAuthProfile(connection, authProfiles, connection.authProfileSlug);
1122
+ if (profile.kind === "oauth_app") {
1123
+ throw new ValidationError(`${connection.sourceFile}: connection "${connection.slug}" \`auth\` references auth profile "${connection.authProfileSlug}" of kind \`oauth_app\` — use \`app_auth\` for OAuth-app credentials and \`auth\` for the account/runtime profile`);
1124
+ }
1125
+ }
1126
+ if (connection.appAuthProfileSlug) {
1127
+ const profile = requireAuthProfile(connection, authProfiles, connection.appAuthProfileSlug);
1128
+ if (profile.kind !== "oauth_app") {
1129
+ throw new ValidationError(`${connection.sourceFile}: connection "${connection.slug}" \`app_auth\` must reference an \`oauth_app\` auth profile (got \`${profile.kind}\`)`);
1130
+ }
1131
+ }
1132
+ }
1133
+ function requireAuthProfile(connection, authProfiles, slugRef) {
1134
+ const profile = authProfiles.get(slugRef);
1135
+ if (!profile) {
1136
+ throw new ValidationError(`${connection.sourceFile}: connection "${connection.slug}" references auth profile "${slugRef}" which is not declared in any \`type: auth_profile\` doc`);
1137
+ }
1138
+ if (profile.connector !== connection.connector) {
1139
+ throw new ValidationError(`${connection.sourceFile}: connection "${connection.slug}" references auth profile "${slugRef}" for connector "${profile.connector}", but the connection uses connector "${connection.connector}"`);
1140
+ }
1141
+ return profile;
1142
+ }
1143
+ export function validateAuthProfileAgainstConnector(profile, schemas) {
1144
+ if (!schemas)
1145
+ return;
1146
+ if (schemas.authKinds.size > 0 && !schemas.authKinds.has(profile.kind)) {
1147
+ throw new ValidationError(`${profile.sourceFile}: auth_profile "${profile.slug}" uses \`kind: ${profile.kind}\`, but connector "${profile.connector}" supports: ${[...schemas.authKinds].sort().join(", ") || "(none)"}`);
1148
+ }
1149
+ }
559
1150
  export async function loadDesiredState(opts) {
560
1151
  const result = await loadConfig(opts.cwd);
561
1152
  if (isLoadError(result)) {
@@ -587,11 +1178,34 @@ export async function loadDesiredState(opts) {
587
1178
  metadata.description = agentConfig.description;
588
1179
  agents.push({ metadata, settings, platforms });
589
1180
  }
590
- const memorySchema = await loadMemorySchema(config, opts.cwd);
1181
+ const { entityTypes, relationshipTypes, watchers } = await loadMemoryModels(config, opts.cwd);
1182
+ for (const watcher of watchers) {
1183
+ if (!config.agents[watcher.agent]) {
1184
+ throw new ValidationError(`watcher "${watcher.slug}" names agent "${watcher.agent}", but there is no \`[agents.${watcher.agent}]\` block in lobu.toml`);
1185
+ }
1186
+ }
1187
+ const connectors = opts.only
1188
+ ? { definitions: [], authProfiles: [], connections: [] }
1189
+ : await loadConnectors(config, opts.cwd, env, requiredSecrets);
1190
+ const memory = config.memory && config.memory.enabled !== false
1191
+ ? {
1192
+ ...(config.memory.org ? { org: config.memory.org } : {}),
1193
+ ...(config.memory.organization_id
1194
+ ? { organizationId: config.memory.organization_id }
1195
+ : {}),
1196
+ ...(config.memory.name ? { name: config.memory.name } : {}),
1197
+ ...(config.memory.description
1198
+ ? { description: config.memory.description }
1199
+ : {}),
1200
+ }
1201
+ : undefined;
591
1202
  return {
592
1203
  state: {
593
1204
  agents,
594
- memorySchema,
1205
+ ...(memory ? { memory } : {}),
1206
+ memorySchema: { entityTypes, relationshipTypes },
1207
+ watchers,
1208
+ connectors,
595
1209
  requiredSecrets: [...requiredSecrets].sort(),
596
1210
  },
597
1211
  configPath,