@lobu/cli 6.1.1 → 7.1.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 (177) hide show
  1. package/dist/commands/_lib/apply/apply-cmd.d.ts +36 -0
  2. package/dist/commands/_lib/apply/apply-cmd.d.ts.map +1 -1
  3. package/dist/commands/_lib/apply/apply-cmd.js +696 -40
  4. package/dist/commands/_lib/apply/apply-cmd.js.map +1 -1
  5. package/dist/commands/_lib/apply/client.d.ts +285 -0
  6. package/dist/commands/_lib/apply/client.d.ts.map +1 -1
  7. package/dist/commands/_lib/apply/client.js +469 -28
  8. package/dist/commands/_lib/apply/client.js.map +1 -1
  9. package/dist/commands/_lib/apply/desired-state.d.ts +187 -3
  10. package/dist/commands/_lib/apply/desired-state.d.ts.map +1 -1
  11. package/dist/commands/_lib/apply/desired-state.js +879 -88
  12. package/dist/commands/_lib/apply/desired-state.js.map +1 -1
  13. package/dist/commands/_lib/apply/diff.d.ts +72 -3
  14. package/dist/commands/_lib/apply/diff.d.ts.map +1 -1
  15. package/dist/commands/_lib/apply/diff.js +473 -84
  16. package/dist/commands/_lib/apply/diff.js.map +1 -1
  17. package/dist/commands/_lib/apply/prompt.d.ts +6 -0
  18. package/dist/commands/_lib/apply/prompt.d.ts.map +1 -1
  19. package/dist/commands/_lib/apply/prompt.js +16 -0
  20. package/dist/commands/_lib/apply/prompt.js.map +1 -1
  21. package/dist/commands/_lib/apply/render.d.ts +9 -0
  22. package/dist/commands/_lib/apply/render.d.ts.map +1 -1
  23. package/dist/commands/_lib/apply/render.js +80 -3
  24. package/dist/commands/_lib/apply/render.js.map +1 -1
  25. package/dist/commands/_lib/connector-loader.d.ts +3 -0
  26. package/dist/commands/_lib/connector-loader.d.ts.map +1 -0
  27. package/dist/commands/_lib/connector-loader.js +129 -0
  28. package/dist/commands/_lib/connector-loader.js.map +1 -0
  29. package/dist/commands/_lib/connector-run-cmd.d.ts +35 -0
  30. package/dist/commands/_lib/connector-run-cmd.d.ts.map +1 -0
  31. package/dist/commands/_lib/connector-run-cmd.js +351 -0
  32. package/dist/commands/_lib/connector-run-cmd.js.map +1 -0
  33. package/dist/commands/_lib/export/export-cmd.d.ts +35 -0
  34. package/dist/commands/_lib/export/export-cmd.d.ts.map +1 -0
  35. package/dist/commands/_lib/export/export-cmd.js +329 -0
  36. package/dist/commands/_lib/export/export-cmd.js.map +1 -0
  37. package/dist/commands/agent.d.ts.map +1 -1
  38. package/dist/commands/agent.js +11 -14
  39. package/dist/commands/agent.js.map +1 -1
  40. package/dist/commands/chat.d.ts.map +1 -1
  41. package/dist/commands/chat.js +28 -7
  42. package/dist/commands/chat.js.map +1 -1
  43. package/dist/commands/connector.d.ts +3 -0
  44. package/dist/commands/connector.d.ts.map +1 -0
  45. package/dist/commands/connector.js +5 -0
  46. package/dist/commands/connector.js.map +1 -0
  47. package/dist/commands/dev.d.ts +23 -0
  48. package/dist/commands/dev.d.ts.map +1 -1
  49. package/dist/commands/dev.js +273 -8
  50. package/dist/commands/dev.js.map +1 -1
  51. package/dist/commands/doctor.d.ts.map +1 -1
  52. package/dist/commands/doctor.js +2 -3
  53. package/dist/commands/doctor.js.map +1 -1
  54. package/dist/commands/eval.d.ts.map +1 -1
  55. package/dist/commands/eval.js +28 -18
  56. package/dist/commands/eval.js.map +1 -1
  57. package/dist/commands/init.d.ts +2 -0
  58. package/dist/commands/init.d.ts.map +1 -1
  59. package/dist/commands/init.js +29 -1
  60. package/dist/commands/init.js.map +1 -1
  61. package/dist/commands/login.d.ts.map +1 -1
  62. package/dist/commands/login.js +22 -16
  63. package/dist/commands/login.js.map +1 -1
  64. package/dist/commands/memory/_lib/browser-auth-cmd.d.ts.map +1 -1
  65. package/dist/commands/memory/_lib/browser-auth-cmd.js +15 -144
  66. package/dist/commands/memory/_lib/browser-auth-cmd.js.map +1 -1
  67. package/dist/commands/memory/_lib/schema.d.ts +28 -1
  68. package/dist/commands/memory/_lib/schema.d.ts.map +1 -1
  69. package/dist/commands/memory/_lib/schema.js +120 -4
  70. package/dist/commands/memory/_lib/schema.js.map +1 -1
  71. package/dist/commands/memory/_lib/seed-cmd.d.ts.map +1 -1
  72. package/dist/commands/memory/_lib/seed-cmd.js +41 -18
  73. package/dist/commands/memory/_lib/seed-cmd.js.map +1 -1
  74. package/dist/commands/org.d.ts +4 -0
  75. package/dist/commands/org.d.ts.map +1 -1
  76. package/dist/commands/org.js +10 -0
  77. package/dist/commands/org.js.map +1 -1
  78. package/dist/commands/token.d.ts +9 -0
  79. package/dist/commands/token.d.ts.map +1 -1
  80. package/dist/commands/token.js +54 -3
  81. package/dist/commands/token.js.map +1 -1
  82. package/dist/commands/validate.d.ts.map +1 -1
  83. package/dist/commands/validate.js +4 -13
  84. package/dist/commands/validate.js.map +1 -1
  85. package/dist/config/loader.js +2 -2
  86. package/dist/config/loader.js.map +1 -1
  87. package/dist/connectors/README.md +2 -3
  88. package/dist/connectors/apple_health.ts +138 -0
  89. package/dist/connectors/apple_photos.ts +178 -0
  90. package/dist/connectors/apple_screen_time.ts +82 -0
  91. package/dist/connectors/browser/evaluate.ts +120 -0
  92. package/dist/connectors/browser/fill_form.ts +107 -0
  93. package/dist/connectors/browser/page_text.ts +108 -0
  94. package/dist/connectors/browser-scraper-utils.ts +111 -3
  95. package/dist/connectors/capterra.ts +5 -1
  96. package/dist/connectors/chrome_tabs.ts +74 -0
  97. package/dist/connectors/g2.ts +5 -1
  98. package/dist/connectors/github.ts +16 -38
  99. package/dist/connectors/glassdoor.ts +5 -1
  100. package/dist/connectors/google_calendar.ts +28 -6
  101. package/dist/connectors/google_gmail.ts +6 -3
  102. package/dist/connectors/google_play.ts +32 -5
  103. package/dist/connectors/hackernews.ts +37 -2
  104. package/dist/connectors/index.ts +14 -1
  105. package/dist/connectors/linkedin.ts +32 -9
  106. package/dist/connectors/local_directory.ts +91 -0
  107. package/dist/connectors/reddit.ts +1 -0
  108. package/dist/connectors/revolut.ts +569 -0
  109. package/dist/connectors/rss.ts +33 -8
  110. package/dist/connectors/trustpilot.ts +36 -21
  111. package/dist/connectors/website.ts +8 -69
  112. package/dist/connectors/whatsapp.ts +21 -22
  113. package/dist/connectors/whatsapp_local.ts +125 -0
  114. package/dist/connectors/x.ts +17 -7
  115. package/dist/db/migrations/20260510220000_connector_required_capability.sql +47 -0
  116. package/dist/db/migrations/20260512000000_device_worker_connection_binding.sql +113 -0
  117. package/dist/db/migrations/20260512131703_connections_slug.sql +131 -0
  118. package/dist/db/migrations/20260513000000_chat_user_identities.sql +24 -0
  119. package/dist/db/migrations/20260513120000_auth_profiles_device_binding.sql +50 -0
  120. package/dist/db/migrations/20260513150000_auth_profiles_cdp_url.sql +43 -0
  121. package/dist/db/migrations/20260513200000_notifications_as_events.sql +86 -0
  122. package/dist/db/migrations/20260514000000_scheduled_jobs.sql +97 -0
  123. package/dist/db/migrations/20260514120000_auth_profiles_connector_key_nullable.sql +42 -0
  124. package/dist/db/migrations/20260514130000_connection_action_modes.sql +103 -0
  125. package/dist/db/migrations/20260514160000_auth_profiles_mirror_mode.sql +32 -0
  126. package/dist/db/migrations/20260515120000_agents_per_org_pk.sql +66 -0
  127. package/dist/db/migrations/20260515150000_geo_enrichment.sql +208 -0
  128. package/dist/db/migrations/20260515160000_drop_agents_org_id_unique.sql +24 -0
  129. package/dist/db/migrations/20260515170000_auth_profiles_default_for_connector.sql +23 -0
  130. package/dist/db/migrations/20260516120000_agents_per_org_pk_swap.sql +125 -0
  131. package/dist/db/migrations/20260516200000_events_search_tsv.sql +134 -0
  132. package/dist/db/migrations/20260516200100_events_lifecycle_changes_index.sql +25 -0
  133. package/dist/db/migrations/20260517010000_drop_unused_indexes.sql +49 -0
  134. package/dist/db/migrations/20260517020000_softdelete_orphan_feeds.sql +56 -0
  135. package/dist/db/migrations/20260517030000_pat_worker_id_binding.sql +27 -0
  136. package/dist/db/migrations/20260517040000_archive_orphan_watchers.sql +30 -0
  137. package/dist/db/migrations/20260517050000_watcher_agent_id_not_null.sql +34 -0
  138. package/dist/db/migrations/20260517060000_watcher_schema_additions.sql +78 -0
  139. package/dist/db/migrations/20260517150000_goals_primitive.sql +55 -0
  140. package/dist/db/migrations/20260517160000_drop_goals_primitive.sql +45 -0
  141. package/dist/db/migrations/20260518000000_pending_interactions.sql +49 -0
  142. package/dist/db/migrations/20260518010000_runs_heartbeat_reaper_index.sql +22 -0
  143. package/dist/eval/client.d.ts.map +1 -1
  144. package/dist/eval/client.js +11 -0
  145. package/dist/eval/client.js.map +1 -1
  146. package/dist/eval/grader.js +2 -1
  147. package/dist/eval/grader.js.map +1 -1
  148. package/dist/eval/types.d.ts +2 -0
  149. package/dist/eval/types.d.ts.map +1 -1
  150. package/dist/index.d.ts +11 -0
  151. package/dist/index.d.ts.map +1 -1
  152. package/dist/index.js +115 -114
  153. package/dist/index.js.map +1 -1
  154. package/dist/internal/context.d.ts +9 -0
  155. package/dist/internal/context.d.ts.map +1 -1
  156. package/dist/internal/context.js +41 -6
  157. package/dist/internal/context.js.map +1 -1
  158. package/dist/internal/credentials.d.ts +5 -0
  159. package/dist/internal/credentials.d.ts.map +1 -1
  160. package/dist/internal/credentials.js +75 -1
  161. package/dist/internal/credentials.js.map +1 -1
  162. package/dist/internal/gateway-url.d.ts +14 -0
  163. package/dist/internal/gateway-url.d.ts.map +1 -1
  164. package/dist/internal/gateway-url.js +19 -0
  165. package/dist/internal/gateway-url.js.map +1 -1
  166. package/dist/internal/index.d.ts +1 -1
  167. package/dist/internal/index.d.ts.map +1 -1
  168. package/dist/internal/index.js +1 -1
  169. package/dist/internal/index.js.map +1 -1
  170. package/dist/internal/local-env.d.ts.map +1 -1
  171. package/dist/internal/local-env.js +9 -2
  172. package/dist/internal/local-env.js.map +1 -1
  173. package/dist/server.bundle.mjs +42251 -36931
  174. package/dist/start-local.bundle.mjs +16437 -9882
  175. package/dist/templates/TESTING.md.tmpl +9 -9
  176. package/package.json +8 -6
  177. package/dist/connectors/google_photos.ts +0 -776
@@ -1,3 +1,4 @@
1
+ import { ValidationError } from "../../memory/_lib/errors.js";
1
2
  // ── Equality helpers ───────────────────────────────────────────────────────
2
3
  /**
3
4
  * Stable structural equality for JSON-shaped values. Sorts object keys before
@@ -25,35 +26,68 @@ function canonical(value) {
25
26
  }
26
27
  return JSON.stringify(value);
27
28
  }
28
- // ── Per-resource diff ──────────────────────────────────────────────────────
29
- function diffAgent(desired, remote) {
30
- if (!remote) {
31
- return { kind: "agent", verb: "create", id: desired.agentId, desired };
32
- }
33
- const changed = [];
34
- if (desired.name !== remote.name)
35
- changed.push("name");
36
- if ((desired.description ?? "") !== (remote.description ?? "")) {
37
- changed.push("description");
29
+ /** `(a ?? "") !== (b ?? "")` — the canonical optional-string comparison. */
30
+ function stringChanged(a, b) {
31
+ return (a ?? "") !== (b ?? "");
32
+ }
33
+ /**
34
+ * The shared create / noop / update shape behind most `diffX` functions.
35
+ * `extras` is merged into every row (create/noop/update); `updateExtras`
36
+ * adds verb-specific props derived from the changed-field list (e.g.
37
+ * `willRestart`). `changedFields` is attached automatically on update.
38
+ */
39
+ function buildDiffRow(opts) {
40
+ const extras = opts.extras ?? {};
41
+ if (!opts.remote) {
42
+ return {
43
+ kind: opts.kind,
44
+ verb: "create",
45
+ id: opts.id,
46
+ desired: opts.desired,
47
+ ...extras,
48
+ };
38
49
  }
50
+ const remote = opts.remote;
51
+ const changed = opts.fields
52
+ .filter((f) => f.changed(opts.desired, remote))
53
+ .map((f) => f.name);
39
54
  if (changed.length === 0) {
40
55
  return {
41
- kind: "agent",
56
+ kind: opts.kind,
42
57
  verb: "noop",
43
- id: desired.agentId,
44
- desired,
58
+ id: opts.id,
59
+ desired: opts.desired,
45
60
  remote,
61
+ ...extras,
46
62
  };
47
63
  }
48
64
  return {
49
- kind: "agent",
65
+ kind: opts.kind,
50
66
  verb: "update",
51
- id: desired.agentId,
52
- desired,
67
+ id: opts.id,
68
+ desired: opts.desired,
53
69
  remote,
54
70
  changedFields: changed,
71
+ ...extras,
72
+ ...(opts.updateExtras?.(changed) ?? {}),
55
73
  };
56
74
  }
75
+ // ── Per-resource diff ──────────────────────────────────────────────────────
76
+ function diffAgent(desired, remote) {
77
+ return buildDiffRow({
78
+ kind: "agent",
79
+ id: desired.agentId,
80
+ desired,
81
+ remote,
82
+ fields: [
83
+ { name: "name", changed: (d, r) => d.name !== r.name },
84
+ {
85
+ name: "description",
86
+ changed: (d, r) => stringChanged(d.description, r.description),
87
+ },
88
+ ],
89
+ });
90
+ }
57
91
  /**
58
92
  * Compare desired settings against what's currently stored.
59
93
  *
@@ -117,67 +151,249 @@ function diffSettings(agentId, desired, remote) {
117
151
  };
118
152
  }
119
153
  function diffPlatform(agentId, desired, remote) {
154
+ return buildDiffRow({
155
+ kind: "platform",
156
+ id: desired.stableId,
157
+ desired,
158
+ remote,
159
+ extras: remote ? { agentId } : { agentId, willRestart: false },
160
+ fields: [
161
+ { name: "type", changed: (d, r) => d.type !== r.platform },
162
+ {
163
+ name: "config",
164
+ changed: (d, r) => {
165
+ // The route handler stores `platform` inside `config` for stable-id
166
+ // matching, so a noop round-trip from GET will have an extra
167
+ // `platform` key the CLI never wrote. Strip it before diffing so an
168
+ // unchanged platform doesn't show as drift on every plan.
169
+ const remoteConfig = { ...(r.config ?? {}) };
170
+ delete remoteConfig.platform;
171
+ return !deepEqual(d.config, remoteConfig);
172
+ },
173
+ },
174
+ ],
175
+ updateExtras: (changed) => ({
176
+ willRestart: changed.includes("config") || changed.includes("type"),
177
+ }),
178
+ });
179
+ }
180
+ function diffEntityType(desired, remote) {
181
+ return buildDiffRow({
182
+ kind: "entity-type",
183
+ id: desired.slug,
184
+ desired,
185
+ remote,
186
+ fields: [
187
+ { name: "name", changed: (d, r) => stringChanged(d.name, r.name) },
188
+ {
189
+ name: "description",
190
+ changed: (d, r) => stringChanged(d.description, r.description),
191
+ },
192
+ {
193
+ name: "required",
194
+ changed: (d, r) => !deepEqual(d.required ?? [], r.required ?? []),
195
+ },
196
+ {
197
+ name: "properties",
198
+ changed: (d, r) => !deepEqual(d.properties, r.properties),
199
+ },
200
+ ],
201
+ });
202
+ }
203
+ function diffRelationshipType(desired, remote) {
204
+ return buildDiffRow({
205
+ kind: "relationship-type",
206
+ id: desired.slug,
207
+ desired,
208
+ remote,
209
+ fields: [
210
+ { name: "name", changed: (d, r) => stringChanged(d.name, r.name) },
211
+ {
212
+ name: "description",
213
+ changed: (d, r) => stringChanged(d.description, r.description),
214
+ },
215
+ {
216
+ name: "rules",
217
+ changed: (d, r) => !deepEqual(d.rules ?? [], r.rules ?? []),
218
+ },
219
+ ],
220
+ });
221
+ }
222
+ /**
223
+ * Watcher drift fields split into two routing categories:
224
+ * - **scalar** lives on the `watchers` row → `manage_watchers update`.
225
+ * - **version-bound** lives on the `watcher_versions` row → must go through
226
+ * `create_version` + `upgrade` (server-side bumps `current_version_id`).
227
+ * The diff returns both lists; apply-cmd routes accordingly.
228
+ *
229
+ * Reaction scripts aren't returned by `list_watchers` (write-only on the row),
230
+ * so we can't compare them — apply always re-pushes when declared (idempotent).
231
+ * Remote watchers without a desired model are reported as drift, never deleted.
232
+ */
233
+ function diffWatcher(desired, remote) {
234
+ const reactionScriptDeclared = desired.reactionScript !== undefined;
120
235
  if (!remote) {
121
236
  return {
122
- kind: "platform",
237
+ kind: "watcher",
123
238
  verb: "create",
124
- id: desired.stableId,
125
- agentId,
239
+ id: desired.slug,
126
240
  desired,
127
- willRestart: false,
241
+ ...(reactionScriptDeclared ? { reactionScriptDeclared: true } : {}),
128
242
  };
129
243
  }
130
- const changed = [];
131
- if (desired.type !== remote.platform)
132
- changed.push("type");
133
- // The route handler stores `platform` inside `config` for stable-id matching,
134
- // so a noop round-trip from GET will have an extra `platform` key the CLI
135
- // never wrote. Strip it before diffing so an unchanged platform doesn't
136
- // show as drift on every plan.
137
- const remoteConfig = { ...(remote.config ?? {}) };
138
- delete remoteConfig.platform;
139
- if (!deepEqual(desired.config, remoteConfig))
140
- changed.push("config");
244
+ const scalar = [];
245
+ if ((desired.schedule ?? null) !== (remote.schedule ?? null)) {
246
+ scalar.push("schedule");
247
+ }
248
+ if (desired.agent !== (remote.agent_id ?? "")) {
249
+ scalar.push("agent_id");
250
+ }
251
+ if (desired.deviceWorkerId !== undefined &&
252
+ desired.deviceWorkerId !== (remote.device_worker_id ?? undefined)) {
253
+ scalar.push("device_worker_id");
254
+ }
255
+ if (desired.schedulerClientId !== undefined &&
256
+ desired.schedulerClientId !== (remote.scheduler_client_id ?? undefined)) {
257
+ scalar.push("scheduler_client_id");
258
+ }
259
+ if (desired.notificationChannel !== undefined &&
260
+ desired.notificationChannel !== (remote.notification_channel ?? undefined)) {
261
+ scalar.push("notification_channel");
262
+ }
263
+ if (desired.notificationPriority !== undefined &&
264
+ desired.notificationPriority !== (remote.notification_priority ?? undefined)) {
265
+ scalar.push("notification_priority");
266
+ }
267
+ if (desired.minCooldownSeconds !== undefined &&
268
+ desired.minCooldownSeconds !== (remote.min_cooldown_seconds ?? undefined)) {
269
+ scalar.push("min_cooldown_seconds");
270
+ }
271
+ if (desired.tags !== undefined &&
272
+ !deepEqual(desired.tags, remote.tags ?? [])) {
273
+ scalar.push("tags");
274
+ }
275
+ if (desired.agentKind !== undefined &&
276
+ desired.agentKind !== (remote.agent_kind ?? undefined)) {
277
+ scalar.push("agent_kind");
278
+ }
279
+ const versionBound = [];
280
+ if (desired.prompt !== (remote.prompt ?? "")) {
281
+ versionBound.push("prompt");
282
+ }
283
+ if (!deepEqual(desired.extractionSchema ?? {}, remote.extraction_schema ?? {})) {
284
+ versionBound.push("extraction_schema");
285
+ }
286
+ // Sources live on the watchers row but are written as part of create_version
287
+ // when changed (server copies them to the version's per-assignment scope).
288
+ // Diff against `remote.sources` (also from the row) and route through
289
+ // create_version so the version chain stays consistent.
290
+ if (desired.sources !== undefined &&
291
+ !deepEqual(desired.sources, remote.sources ?? [])) {
292
+ versionBound.push("sources");
293
+ }
294
+ if (desired.reactionsGuidance !== undefined &&
295
+ desired.reactionsGuidance !== (remote.reactions_guidance ?? "")) {
296
+ versionBound.push("reactions_guidance");
297
+ }
298
+ if (desired.jsonTemplate !== undefined &&
299
+ !deepEqual(desired.jsonTemplate, remote.json_template)) {
300
+ versionBound.push("json_template");
301
+ }
302
+ if (desired.keyingConfig !== undefined &&
303
+ !deepEqual(desired.keyingConfig, remote.keying_config ?? {})) {
304
+ versionBound.push("keying_config");
305
+ }
306
+ if (desired.classifiers !== undefined &&
307
+ !deepEqual(desired.classifiers, remote.classifiers ?? [])) {
308
+ versionBound.push("classifiers");
309
+ }
310
+ if (desired.condensationPrompt !== undefined &&
311
+ desired.condensationPrompt !== (remote.condensation_prompt ?? "")) {
312
+ versionBound.push("condensation_prompt");
313
+ }
314
+ if (desired.condensationWindowCount !== undefined &&
315
+ desired.condensationWindowCount !==
316
+ (remote.condensation_window_count ?? undefined)) {
317
+ versionBound.push("condensation_window_count");
318
+ }
319
+ const changed = [...scalar, ...versionBound];
320
+ if (reactionScriptDeclared)
321
+ changed.push("reaction_script");
141
322
  if (changed.length === 0) {
142
- return {
143
- kind: "platform",
144
- verb: "noop",
145
- id: desired.stableId,
146
- agentId,
147
- desired,
148
- remote,
149
- };
323
+ return { kind: "watcher", verb: "noop", id: desired.slug, desired, remote };
150
324
  }
151
325
  return {
152
- kind: "platform",
326
+ kind: "watcher",
153
327
  verb: "update",
154
- id: desired.stableId,
155
- agentId,
328
+ id: desired.slug,
156
329
  desired,
157
330
  remote,
158
331
  changedFields: changed,
159
- willRestart: changed.includes("config") || changed.includes("type"),
332
+ ...(versionBound.length > 0 ? { versionBoundFields: versionBound } : {}),
333
+ ...(reactionScriptDeclared ? { reactionScriptDeclared: true } : {}),
160
334
  };
161
335
  }
162
- function diffEntityType(desired, remote) {
336
+ // ── Connectors ─────────────────────────────────────────────────────────────
337
+ const INTERACTIVE_AUTH_KINDS = new Set([
338
+ "oauth_account",
339
+ "browser_session",
340
+ ]);
341
+ function connectorDefinitionId(def) {
342
+ return def.key ?? def.sourceFile;
343
+ }
344
+ function diffConnectorDefinition(desired, installedKeys) {
345
+ const id = connectorDefinitionId(desired);
346
+ // The CLI can't compare source hashes (the server compiles, and the stored
347
+ // hash is of the *compiled* output) — so we always emit a "sync" row;
348
+ // `install_connector` is idempotent and reports `updated:false` on apply
349
+ // when the code is byte-identical, so this never churns remote state.
350
+ const installedRemotely = desired.key
351
+ ? installedKeys.has(desired.key)
352
+ : false;
353
+ return {
354
+ kind: "connector-definition",
355
+ // "update" (re-push) when already installed; `install_connector` is
356
+ // idempotent and reports `updated:false` if the code is unchanged.
357
+ verb: installedRemotely ? "update" : "create",
358
+ id,
359
+ desired,
360
+ installedRemotely,
361
+ };
362
+ }
363
+ function diffAuthProfile(desired, remote) {
163
364
  if (!remote) {
164
- return { kind: "entity-type", verb: "create", id: desired.slug, desired };
365
+ return {
366
+ kind: "auth-profile",
367
+ verb: "create",
368
+ id: desired.slug,
369
+ desired,
370
+ needsAuth: INTERACTIVE_AUTH_KINDS.has(desired.kind),
371
+ };
372
+ }
373
+ // `connector` / `kind` are immutable — `update_auth_profile` can't change
374
+ // them, so reusing a slug for a different connector/kind would silently push
375
+ // credentials into the wrong profile. Hard-stop instead.
376
+ if (remote.connector_key !== desired.connector ||
377
+ remote.profile_kind !== desired.kind) {
378
+ throw new ValidationError(`${desired.sourceFile}: auth_profile "${desired.slug}" is bound to ${remote.connector_key}/${remote.profile_kind} remotely, but the manifest declares ${desired.connector}/${desired.kind} — delete it manually or use a new slug`);
165
379
  }
166
380
  const changed = [];
167
- if ((desired.name ?? "") !== (remote.name ?? ""))
381
+ if ((desired.name ?? "") !== (remote.display_name ?? "")) {
168
382
  changed.push("name");
169
- if ((desired.description ?? "") !== (remote.description ?? "")) {
170
- changed.push("description");
171
- }
172
- if (!deepEqual(desired.required ?? [], remote.required ?? [])) {
173
- changed.push("required");
174
383
  }
175
- if (!deepEqual(desired.properties, remote.properties)) {
176
- changed.push("properties");
177
- }
178
- if (changed.length === 0) {
384
+ // Credentials can't be read back from the server (write-only secrets). For
385
+ // non-interactive kinds with declared credentials, always re-push them
386
+ // (idempotent) so rotated secrets propagate — show as a redacted "credentials"
387
+ // change. Interactive kinds never carry credentials in the manifest.
388
+ const declaresCredentials = !INTERACTIVE_AUTH_KINDS.has(desired.kind) &&
389
+ desired.credentials !== undefined &&
390
+ Object.keys(desired.credentials).length > 0;
391
+ if (declaresCredentials)
392
+ changed.push("credentials");
393
+ const needsAuth = INTERACTIVE_AUTH_KINDS.has(desired.kind) && remote.status !== "active";
394
+ if (changed.length === 0 && !needsAuth) {
179
395
  return {
180
- kind: "entity-type",
396
+ kind: "auth-profile",
181
397
  verb: "noop",
182
398
  id: desired.slug,
183
399
  desired,
@@ -185,49 +401,81 @@ function diffEntityType(desired, remote) {
185
401
  };
186
402
  }
187
403
  return {
188
- kind: "entity-type",
189
- verb: "update",
404
+ kind: "auth-profile",
405
+ verb: changed.length > 0 ? "update" : "noop",
190
406
  id: desired.slug,
191
407
  desired,
192
408
  remote,
193
- changedFields: changed,
409
+ ...(changed.length > 0 ? { changedFields: changed } : {}),
410
+ ...(needsAuth ? { needsAuth: true } : {}),
194
411
  };
195
412
  }
196
- function diffRelationshipType(desired, remote) {
413
+ function diffConnection(desired, remote) {
197
414
  if (!remote) {
198
415
  return {
199
- kind: "relationship-type",
416
+ kind: "connection",
200
417
  verb: "create",
201
418
  id: desired.slug,
202
419
  desired,
203
420
  };
204
421
  }
205
- const changed = [];
206
- if ((desired.name ?? "") !== (remote.name ?? ""))
207
- changed.push("name");
208
- if ((desired.description ?? "") !== (remote.description ?? "")) {
209
- changed.push("description");
210
- }
211
- if (!deepEqual(desired.rules ?? [], remote.rules ?? [])) {
212
- changed.push("rules");
422
+ // `connector` is immutable — `update` can't change `connector_key`, so a
423
+ // slug bound to a different connector remotely must be a hard error, never
424
+ // an "update" that mutates auth/config on the wrong connector.
425
+ if (remote.connector_key !== desired.connector) {
426
+ throw new ValidationError(`${desired.sourceFile}: connection "${desired.slug}" is bound to connector "${remote.connector_key}" remotely, but the manifest declares "${desired.connector}" — delete it manually or use a new slug`);
213
427
  }
214
- if (changed.length === 0) {
215
- return {
216
- kind: "relationship-type",
217
- verb: "noop",
218
- id: desired.slug,
219
- desired,
220
- remote,
221
- };
222
- }
223
- return {
224
- kind: "relationship-type",
225
- verb: "update",
428
+ return buildDiffRow({
429
+ kind: "connection",
226
430
  id: desired.slug,
227
431
  desired,
228
432
  remote,
229
- changedFields: changed,
230
- };
433
+ fields: [
434
+ {
435
+ name: "name",
436
+ changed: (d, r) => stringChanged(d.name, r.display_name),
437
+ },
438
+ {
439
+ name: "auth",
440
+ changed: (d, r) => (d.authProfileSlug ?? null) !== (r.auth_profile_slug ?? null),
441
+ },
442
+ {
443
+ name: "app_auth",
444
+ changed: (d, r) => (d.appAuthProfileSlug ?? null) !== (r.app_auth_profile_slug ?? null),
445
+ },
446
+ {
447
+ name: "config",
448
+ changed: (d, r) => !deepEqual(d.config ?? {}, r.config ?? {}),
449
+ },
450
+ {
451
+ name: "device_worker_id",
452
+ changed: (d, r) => (d.deviceWorkerId ?? null) !== (r.device_worker_id ?? null),
453
+ },
454
+ ],
455
+ });
456
+ }
457
+ function diffFeed(connectionSlug, desired, remote) {
458
+ return buildDiffRow({
459
+ kind: "feed",
460
+ id: `${connectionSlug}/${desired.feedKey}`,
461
+ desired,
462
+ remote,
463
+ extras: { connectionSlug },
464
+ fields: [
465
+ {
466
+ name: "name",
467
+ changed: (d, r) => stringChanged(d.name, r.display_name),
468
+ },
469
+ {
470
+ name: "schedule",
471
+ changed: (d, r) => (d.schedule ?? null) !== (r.schedule ?? null),
472
+ },
473
+ {
474
+ name: "config",
475
+ changed: (d, r) => !deepEqual(d.config ?? {}, r.config ?? {}),
476
+ },
477
+ ],
478
+ });
231
479
  }
232
480
  export function computeDiff(desired, remote, opts = {}) {
233
481
  const rows = [];
@@ -314,10 +562,151 @@ export function computeDiff(desired, remote, opts = {}) {
314
562
  });
315
563
  }
316
564
  }
565
+ const remoteWatcherBySlug = new Map(remote.watchers.map((w) => [w.slug, w]));
566
+ const desiredWatcherSlugs = new Set(desired.watchers.map((w) => w.slug));
567
+ for (const watcher of desired.watchers) {
568
+ rows.push(diffWatcher(watcher, remoteWatcherBySlug.get(watcher.slug)));
569
+ }
570
+ for (const remoteWatcher of remote.watchers) {
571
+ if (!desiredWatcherSlugs.has(remoteWatcher.slug)) {
572
+ rows.push({
573
+ kind: "watcher",
574
+ verb: "drift",
575
+ id: remoteWatcher.slug,
576
+ remote: remoteWatcher,
577
+ });
578
+ }
579
+ }
580
+ }
581
+ const notes = [];
582
+ // Connectors run only on a full apply (`--only agents|memory` skips them).
583
+ const desiredConnectors = desired.connectors ?? {
584
+ definitions: [],
585
+ authProfiles: [],
586
+ connections: [],
587
+ };
588
+ // Sort remote collections so drift rows + notes render deterministically
589
+ // regardless of server response ordering.
590
+ const remoteConnectorDefinitions = [
591
+ ...(remote.connectorDefinitions ?? []),
592
+ ].sort((a, b) => a.key.localeCompare(b.key));
593
+ const remoteAuthProfiles = [...(remote.authProfiles ?? [])].sort((a, b) => a.slug.localeCompare(b.slug));
594
+ const remoteConnections = [...(remote.connections ?? [])].sort((a, b) => a.slug.localeCompare(b.slug));
595
+ const remoteFeedsByConnectionId = remote.feedsByConnectionId ?? new Map();
596
+ if (!only) {
597
+ const installedKeys = new Set(remoteConnectorDefinitions.filter((d) => d.installed).map((d) => d.key));
598
+ const declaredKeys = new Set(desiredConnectors.definitions
599
+ .map((d) => d.key)
600
+ .filter((k) => !!k));
601
+ // Connectors referenced by a desired auth profile / connection are (or
602
+ // will be) installed in the org too — bundled ones included — so they
603
+ // aren't "undeclared".
604
+ const referencedConnectorKeys = new Set([
605
+ ...desiredConnectors.authProfiles.map((p) => p.connector),
606
+ ...desiredConnectors.connections.map((c) => c.connector),
607
+ ]);
608
+ // Auto-discovered `.connector.ts` files whose key the CLI can't know up
609
+ // front (the server compiles them). When any exist, suppress the
610
+ // "undeclared remote connector" notes entirely — we can't tell which
611
+ // remote connector corresponds to which local file.
612
+ const hasUnnamedLocalDefs = desiredConnectors.definitions.some((d) => d.key === null);
613
+ for (const def of desiredConnectors.definitions) {
614
+ rows.push(diffConnectorDefinition(def, installedKeys));
615
+ }
616
+ // Bundled connectors referenced by a desired auth-profile / connection that
617
+ // the org doesn't have installed yet — `lobu apply` will install them from
618
+ // the catalog's server-side `source_uri`. Surface them as plan rows so the
619
+ // operator approves these connector-definition mutations too. (Skipped when
620
+ // a locally-supplied connector declares the same key — that one wins.)
621
+ const installableByKey = new Map(remoteConnectorDefinitions
622
+ .filter((d) => d.installable && d.source_uri)
623
+ .map((d) => [d.key, d]));
624
+ for (const key of [...referencedConnectorKeys].sort()) {
625
+ if (installedKeys.has(key))
626
+ continue;
627
+ if (declaredKeys.has(key))
628
+ continue; // a local def supplies this key
629
+ const entry = installableByKey.get(key);
630
+ if (!entry?.source_uri)
631
+ continue;
632
+ rows.push({
633
+ kind: "connector-definition",
634
+ verb: "create",
635
+ id: key,
636
+ // No `desired` — this is a bundled install, handled by
637
+ // `installConnectorDefinitions`'s bundled-referenced loop, not the
638
+ // plan-row loop. The render falls back to `id` (the connector key).
639
+ });
640
+ }
641
+ // Conservative: never auto-uninstall remote connector definitions that
642
+ // aren't declared/referenced locally — just note them.
643
+ if (!hasUnnamedLocalDefs) {
644
+ for (const def of remoteConnectorDefinitions) {
645
+ if (def.installed &&
646
+ !declaredKeys.has(def.key) &&
647
+ !referencedConnectorKeys.has(def.key)) {
648
+ notes.push(`connector "${def.key}" is installed remotely but not declared in connectors/ — uninstall it manually if it's no longer wanted (lobu apply never auto-uninstalls connectors).`);
649
+ }
650
+ }
651
+ }
652
+ const remoteAuthBySlug = new Map(remoteAuthProfiles.map((p) => [p.slug, p]));
653
+ const desiredAuthSlugs = new Set(desiredConnectors.authProfiles.map((p) => p.slug));
654
+ for (const profile of desiredConnectors.authProfiles) {
655
+ rows.push(diffAuthProfile(profile, remoteAuthBySlug.get(profile.slug)));
656
+ }
657
+ for (const remoteProfile of remoteAuthProfiles) {
658
+ if (!desiredAuthSlugs.has(remoteProfile.slug)) {
659
+ rows.push({
660
+ kind: "auth-profile",
661
+ verb: "drift",
662
+ id: remoteProfile.slug,
663
+ remote: remoteProfile,
664
+ });
665
+ }
666
+ }
667
+ const remoteConnBySlug = new Map(remoteConnections.map((c) => [c.slug, c]));
668
+ const desiredConnSlugs = new Set(desiredConnectors.connections.map((c) => c.slug));
669
+ for (const conn of desiredConnectors.connections) {
670
+ const remoteConn = remoteConnBySlug.get(conn.slug);
671
+ rows.push(diffConnection(conn, remoteConn));
672
+ // Nested feeds — diffed by feed_key within the connection. Only diffable
673
+ // when the connection already exists remotely; for a new connection the
674
+ // feeds are created right after the connection-creation step.
675
+ const remoteFeeds = remoteConn
676
+ ? [...(remoteFeedsByConnectionId.get(remoteConn.id) ?? [])].sort((a, b) => a.feed_key.localeCompare(b.feed_key))
677
+ : [];
678
+ const remoteFeedByKey = new Map(remoteFeeds.map((f) => [f.feed_key, f]));
679
+ const desiredFeedKeys = new Set(conn.feeds.map((f) => f.feedKey));
680
+ for (const feed of conn.feeds) {
681
+ rows.push(diffFeed(conn.slug, feed, remoteFeedByKey.get(feed.feedKey)));
682
+ }
683
+ for (const remoteFeed of remoteFeeds) {
684
+ if (!desiredFeedKeys.has(remoteFeed.feed_key)) {
685
+ rows.push({
686
+ kind: "feed",
687
+ verb: "drift",
688
+ id: `${conn.slug}/${remoteFeed.feed_key}`,
689
+ connectionSlug: conn.slug,
690
+ remote: remoteFeed,
691
+ });
692
+ }
693
+ }
694
+ }
695
+ for (const remoteConn of remoteConnections) {
696
+ if (!desiredConnSlugs.has(remoteConn.slug)) {
697
+ rows.push({
698
+ kind: "connection",
699
+ verb: "drift",
700
+ id: remoteConn.slug,
701
+ remote: remoteConn,
702
+ });
703
+ }
704
+ }
317
705
  }
318
706
  const counts = { create: 0, update: 0, noop: 0, drift: 0 };
319
707
  for (const row of rows)
320
708
  counts[row.verb]++;
321
- return { rows, counts };
709
+ notes.sort();
710
+ return { rows, counts, notes };
322
711
  }
323
712
  //# sourceMappingURL=diff.js.map