@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
@@ -0,0 +1,208 @@
1
+ -- migrate:up
2
+
3
+ -- =============================================================================
4
+ -- Geo enrichment: reverse-geocode lat/lng → country / admin1 / place at the
5
+ -- gateway, once, for every event with coordinates. Used by `apple.photos`
6
+ -- today; gmaps reviews, github commit metadata, and any future geo-bearing
7
+ -- connector benefit automatically.
8
+ --
9
+ -- Three system-level reference tables (no organization_id — these are
10
+ -- read-only geographic facts shared across all tenants) seeded from
11
+ -- GeoNames (https://www.geonames.org/, CC-BY 4.0):
12
+ --
13
+ -- geo_countries — country_code → name/continent/currency/etc. (~250 rows)
14
+ -- geo_admin1 — state/province codes per country (~4k rows)
15
+ -- geo_places — populated places (cities/towns/villages/hamlets);
16
+ -- seeded from GeoNames cities1000.txt (~150k rows for v1).
17
+ -- Can be upgraded to the full PPL-class subset of
18
+ -- allCountries (~5M rows) without schema changes —
19
+ -- nearest-neighbor query is the same shape.
20
+ --
21
+ -- The `geo_lookup(lat, lng)` function returns the enriched row in one
22
+ -- call. Nearest-neighbor uses PostGIS `geography(POINT, 4326)` + GiST so
23
+ -- distance is true geodesic (not L2-on-degrees), and the index keeps it
24
+ -- sub-millisecond at any table size we'd ever load.
25
+ --
26
+ -- Run `scripts/seed-geo-data.sh` after this migration applies to populate
27
+ -- the tables. The TS enrichment hook gracefully no-ops if the tables are
28
+ -- empty or the function is missing, so partially-deployed installs keep
29
+ -- working — events just don't get the enriched fields until seeding runs.
30
+ --
31
+ -- ENVIRONMENTS WITHOUT POSTGIS: the entire migration is wrapped in a DO
32
+ -- block that probes for the extension. If PostGIS isn't installable
33
+ -- (PGlite in tests, restricted hosts), every statement below is skipped
34
+ -- with a NOTICE. The runtime enrichment hook also fails open, so
35
+ -- partially-supported environments keep functioning — they just don't
36
+ -- get geo enrichment.
37
+ -- =============================================================================
38
+
39
+ DO $migration$
40
+ BEGIN
41
+ -- Try to install PostGIS. If it's not available on this host (PGlite
42
+ -- without the postgis extension registered, managed Postgres without
43
+ -- the extension, etc.), bail out cleanly.
44
+ BEGIN
45
+ CREATE EXTENSION IF NOT EXISTS postgis;
46
+ EXCEPTION
47
+ WHEN OTHERS THEN
48
+ RAISE NOTICE
49
+ 'geo-enrichment: PostGIS unavailable (%), skipping geo schema. Runtime enrichment will no-op.',
50
+ SQLERRM;
51
+ RETURN;
52
+ END;
53
+
54
+ -- spatial_ref_sys row for SRID 4326 (WGS-84). Real PostGIS installs
55
+ -- bundle ~8000 standard projections; the pglite-postgis WASM build
56
+ -- ships an empty table to keep the bundle small. Inserting the one
57
+ -- row we use makes nearest-neighbour queries work everywhere; the
58
+ -- ON CONFLICT skips on prod where the row already exists.
59
+ INSERT INTO spatial_ref_sys (srid, auth_name, auth_srid, srtext, proj4text)
60
+ VALUES (
61
+ 4326,
62
+ 'EPSG',
63
+ 4326,
64
+ 'GEOGCS["WGS 84",DATUM["WGS_1984",SPHEROID["WGS 84",6378137,298.257223563,AUTHORITY["EPSG","7030"]],AUTHORITY["EPSG","6326"]],PRIMEM["Greenwich",0,AUTHORITY["EPSG","8901"]],UNIT["degree",0.0174532925199433,AUTHORITY["EPSG","9122"]],AUTHORITY["EPSG","4326"]]',
65
+ '+proj=longlat +datum=WGS84 +no_defs'
66
+ )
67
+ ON CONFLICT (srid) DO NOTHING;
68
+
69
+ -- Everything below this point assumes PostGIS is loaded. EXECUTE-wrapping
70
+ -- the DDL keeps the SQL parser from choking on `geography(POINT, 4326)`
71
+ -- when this whole DO block is parsed before the extension creates the type.
72
+
73
+ -- geo_countries — ISO-2 country code → full record. Source: GeoNames
74
+ -- countryInfo.txt.
75
+ EXECUTE $ddl$
76
+ CREATE TABLE IF NOT EXISTS geo_countries (
77
+ code text PRIMARY KEY,
78
+ code3 text,
79
+ numeric_code integer,
80
+ fips text,
81
+ name text NOT NULL,
82
+ capital text,
83
+ area_sq_km numeric,
84
+ population bigint,
85
+ continent text,
86
+ tld text,
87
+ currency_code text,
88
+ currency_name text,
89
+ phone text,
90
+ postal_code_fmt text,
91
+ postal_code_re text,
92
+ languages text,
93
+ geonameid bigint,
94
+ neighbours text
95
+ )
96
+ $ddl$;
97
+
98
+ -- geo_admin1 — first-order administrative subdivisions.
99
+ -- Code shape: '<ISO2>.<ADMIN1>' (e.g. 'IT.07' = Lazio).
100
+ EXECUTE $ddl$
101
+ CREATE TABLE IF NOT EXISTS geo_admin1 (
102
+ code text PRIMARY KEY,
103
+ country_code text NOT NULL,
104
+ name text NOT NULL,
105
+ ascii_name text NOT NULL,
106
+ geonameid bigint
107
+ )
108
+ $ddl$;
109
+
110
+ EXECUTE 'CREATE INDEX IF NOT EXISTS geo_admin1_country_idx ON geo_admin1 (country_code)';
111
+
112
+ -- geo_places — populated places. `location` is a generated geography
113
+ -- point that the GiST index uses for nearest-neighbour lookup. Stays
114
+ -- sub-ms even at 5M+ rows.
115
+ EXECUTE $ddl$
116
+ CREATE TABLE IF NOT EXISTS geo_places (
117
+ geonameid bigint PRIMARY KEY,
118
+ name text NOT NULL,
119
+ ascii_name text NOT NULL,
120
+ alt_names text,
121
+ latitude double precision NOT NULL,
122
+ longitude double precision NOT NULL,
123
+ feature_class text,
124
+ feature_code text,
125
+ country_code text NOT NULL,
126
+ admin1_code text,
127
+ admin2_code text,
128
+ population bigint DEFAULT 0,
129
+ elevation_m integer,
130
+ timezone text,
131
+ location geography(POINT, 4326)
132
+ GENERATED ALWAYS AS (
133
+ ST_SetSRID(ST_MakePoint(longitude, latitude), 4326)::geography
134
+ ) STORED
135
+ )
136
+ $ddl$;
137
+
138
+ EXECUTE 'CREATE INDEX IF NOT EXISTS geo_places_location_idx ON geo_places USING GIST (location)';
139
+ EXECUTE 'CREATE INDEX IF NOT EXISTS geo_places_country_idx ON geo_places (country_code)';
140
+
141
+ -- geo_lookup(lat, lng) — single-call enrichment.
142
+ -- Returns the nearest populated place plus the country/admin1 join.
143
+ -- distance_km is included so callers can apply their own threshold
144
+ -- (e.g., reject results > 500 km away — ocean/desert coordinates that
145
+ -- would otherwise snap misleadingly to the closest coastal city).
146
+ EXECUTE $fn$
147
+ CREATE OR REPLACE FUNCTION geo_lookup(p_lat double precision, p_lng double precision)
148
+ RETURNS TABLE (
149
+ place_name text,
150
+ place_id bigint,
151
+ country_code text,
152
+ country_name text,
153
+ admin1_code text,
154
+ admin1_name text,
155
+ timezone text,
156
+ population bigint,
157
+ distance_km double precision
158
+ )
159
+ LANGUAGE sql
160
+ STABLE
161
+ PARALLEL SAFE
162
+ AS $body$
163
+ WITH nearest AS (
164
+ SELECT
165
+ p.geonameid,
166
+ p.name,
167
+ p.country_code,
168
+ p.admin1_code,
169
+ p.timezone,
170
+ p.population,
171
+ ST_Distance(
172
+ p.location,
173
+ ST_SetSRID(ST_MakePoint(p_lng, p_lat), 4326)::geography
174
+ ) / 1000.0 AS distance_km
175
+ FROM geo_places p
176
+ ORDER BY p.location <-> ST_SetSRID(ST_MakePoint(p_lng, p_lat), 4326)::geography
177
+ LIMIT 1
178
+ )
179
+ SELECT
180
+ n.name AS place_name,
181
+ n.geonameid AS place_id,
182
+ n.country_code AS country_code,
183
+ c.name AS country_name,
184
+ CASE
185
+ WHEN n.admin1_code IS NULL OR n.admin1_code = '' THEN NULL
186
+ ELSE n.country_code || '.' || n.admin1_code
187
+ END AS admin1_code,
188
+ a.name AS admin1_name,
189
+ n.timezone AS timezone,
190
+ n.population AS population,
191
+ n.distance_km AS distance_km
192
+ FROM nearest n
193
+ LEFT JOIN geo_countries c ON c.code = n.country_code
194
+ LEFT JOIN geo_admin1 a ON a.code = n.country_code || '.' || n.admin1_code
195
+ $body$
196
+ $fn$;
197
+ END
198
+ $migration$;
199
+
200
+ -- migrate:down
201
+
202
+ DROP FUNCTION IF EXISTS geo_lookup(double precision, double precision);
203
+ DROP TABLE IF EXISTS geo_places;
204
+ DROP TABLE IF EXISTS geo_admin1;
205
+ DROP TABLE IF EXISTS geo_countries;
206
+ -- Intentionally do NOT DROP EXTENSION postgis. The extension may be
207
+ -- shared by other tables / future migrations on the same Postgres
208
+ -- instance; rolling back this migration shouldn't take that down.
@@ -0,0 +1,24 @@
1
+ -- migrate:up
2
+ -- Drop the parallel `UNIQUE (organization_id, id)` added in 20260515120000.
3
+ -- It was meant as schema-prep for the eventual PK swap to (organization_id,
4
+ -- id), but it actively broke `ON CONFLICT (id) DO NOTHING/UPDATE` callers.
5
+ --
6
+ -- Why: Postgres' `ON CONFLICT (X)` only suppresses violations of the unique
7
+ -- constraint matching exactly column set X. Adding a second unique constraint
8
+ -- that overlaps with the PK means inserts can fail on the new constraint
9
+ -- before reaching the PK conflict — and ON CONFLICT (id) doesn't catch it.
10
+ -- Surfaced in `__tests__/integration/.../race-mcp` where parallel inserts of
11
+ -- `(org-a, race-mcp-0)` started throwing `agents_organization_id_id_key`
12
+ -- duplicates instead of being silently de-duped by the existing
13
+ -- `ON CONFLICT (id) DO NOTHING` clause.
14
+ --
15
+ -- The PK on `(id)` already enforces global uniqueness, which subsumes
16
+ -- `(organization_id, id)` uniqueness — the new constraint was logically
17
+ -- redundant. Phase C of the per-org PK migration will swap the PK directly
18
+ -- without needing a parallel constraint as a stepping stone.
19
+
20
+ ALTER TABLE agents DROP CONSTRAINT IF EXISTS agents_organization_id_id_key;
21
+
22
+ -- migrate:down
23
+ ALTER TABLE agents
24
+ ADD CONSTRAINT agents_organization_id_id_key UNIQUE (organization_id, id);
@@ -0,0 +1,23 @@
1
+ -- migrate:up
2
+ -- Admin-managed default app profile per (org, connector_key).
3
+ -- Today getPrimaryAuthProfileForKind picks the most-recently-updated active
4
+ -- oauth_app profile for the connector — admins have no way to designate
5
+ -- which profile members should fall through to. The flag lets the admin
6
+ -- pin a chosen profile; the resolver prefers flagged rows first.
7
+ --
8
+ -- Constrained to oauth_app for now since that's the only kind where
9
+ -- "default for connector" is meaningful (env / interactive / browser_session
10
+ -- are picked by other rules — device binding, capture mode, etc.).
11
+
12
+ ALTER TABLE auth_profiles
13
+ ADD COLUMN is_default_for_connector boolean NOT NULL DEFAULT false;
14
+
15
+ CREATE UNIQUE INDEX auth_profiles_default_for_connector_unique
16
+ ON auth_profiles (organization_id, connector_key)
17
+ WHERE is_default_for_connector AND profile_kind = 'oauth_app';
18
+
19
+ -- migrate:down
20
+ DROP INDEX IF EXISTS auth_profiles_default_for_connector_unique;
21
+
22
+ ALTER TABLE auth_profiles
23
+ DROP COLUMN IF EXISTS is_default_for_connector;
@@ -0,0 +1,125 @@
1
+ -- migrate:up
2
+ -- Phase C: swap `agents` PK from globally-unique `id` to per-org composite
3
+ -- `(organization_id, id)`. The application has always treated agents as
4
+ -- org-scoped (every read/list filters by organization_id) but the global PK
5
+ -- silently blocked two orgs from sharing an agent ID — for example, a stale
6
+ -- `food-ordering` in one org would prevent `food-ordering` in another.
7
+ --
8
+ -- Phase A (20260515120000) added the org column + composite indexes on the
9
+ -- 5 FK-holding child tables and backfilled values from agents. Phase B (the
10
+ -- application-code refactor in this PR) plumbs `organization_id` through every
11
+ -- INSERT/UPDATE/DELETE/SELECT touching these tables. This migration is the
12
+ -- final structural swap: it drops the single-column PK + FKs, adds the
13
+ -- composite PK + FKs, and widens the per-(agent,kind,pattern) uniques on
14
+ -- agent_users / agent_grants / grants with organization_id.
15
+
16
+ -- ── 0. Set NOT NULL on the columns Phase A backfilled.
17
+ -- Backfill defensively in case any rows snuck in NULL (e.g. embedded PGlite
18
+ -- installs that bypassed the dbmate runner during a partial state).
19
+ UPDATE public.agent_grants c SET organization_id = a.organization_id FROM public.agents a WHERE c.organization_id IS NULL AND c.agent_id = a.id;
20
+ UPDATE public.agent_connections c SET organization_id = a.organization_id FROM public.agents a WHERE c.organization_id IS NULL AND c.agent_id = a.id;
21
+ UPDATE public.agent_users c SET organization_id = a.organization_id FROM public.agents a WHERE c.organization_id IS NULL AND c.agent_id = a.id;
22
+ UPDATE public.agent_channel_bindings c SET organization_id = a.organization_id FROM public.agents a WHERE c.organization_id IS NULL AND c.agent_id = a.id;
23
+ UPDATE public.grants c SET organization_id = a.organization_id FROM public.agents a WHERE c.organization_id IS NULL AND c.agent_id = a.id;
24
+
25
+ -- Drop any orphan rows (agent_id with no matching agents row). Backfill
26
+ -- can't recover these.
27
+ DELETE FROM public.agent_grants WHERE organization_id IS NULL;
28
+ DELETE FROM public.agent_connections WHERE organization_id IS NULL;
29
+ DELETE FROM public.agent_users WHERE organization_id IS NULL;
30
+ DELETE FROM public.agent_channel_bindings WHERE organization_id IS NULL;
31
+ DELETE FROM public.grants WHERE organization_id IS NULL;
32
+
33
+ ALTER TABLE public.agent_grants ALTER COLUMN organization_id SET NOT NULL;
34
+ ALTER TABLE public.agent_connections ALTER COLUMN organization_id SET NOT NULL;
35
+ ALTER TABLE public.agent_users ALTER COLUMN organization_id SET NOT NULL;
36
+ ALTER TABLE public.agent_channel_bindings ALTER COLUMN organization_id SET NOT NULL;
37
+ ALTER TABLE public.grants ALTER COLUMN organization_id SET NOT NULL;
38
+
39
+ -- ── 1. Drop the 6 single-column FKs into agents(id).
40
+ ALTER TABLE public.agent_grants DROP CONSTRAINT IF EXISTS agent_grants_agent_id_fkey;
41
+ ALTER TABLE public.agent_connections DROP CONSTRAINT IF EXISTS agent_connections_agent_id_fkey;
42
+ ALTER TABLE public.agent_users DROP CONSTRAINT IF EXISTS agent_users_agent_id_fkey;
43
+ ALTER TABLE public.agent_channel_bindings DROP CONSTRAINT IF EXISTS agent_channel_bindings_agent_id_fkey;
44
+ ALTER TABLE public.grants DROP CONSTRAINT IF EXISTS grants_agent_id_fkey;
45
+ ALTER TABLE public.scheduled_jobs DROP CONSTRAINT IF EXISTS scheduled_jobs_agent_fkey;
46
+
47
+ -- ── 2. Drop the unique/PK constraints on child tables that scope to bare agent_id.
48
+ ALTER TABLE public.agent_grants DROP CONSTRAINT IF EXISTS agent_grants_agent_id_pattern_key;
49
+ ALTER TABLE public.agent_users DROP CONSTRAINT IF EXISTS agent_users_pkey;
50
+ ALTER TABLE public.grants DROP CONSTRAINT IF EXISTS grants_pkey;
51
+
52
+ -- ── 3. Swap the PK on agents from (id) to (organization_id, id).
53
+ ALTER TABLE public.agents DROP CONSTRAINT IF EXISTS agents_pkey;
54
+ ALTER TABLE public.agents ADD CONSTRAINT agents_pkey PRIMARY KEY (organization_id, id);
55
+
56
+ -- ── 4. Re-add per-org-scoped uniques on the child tables.
57
+ ALTER TABLE public.agent_grants
58
+ ADD CONSTRAINT agent_grants_org_agent_pattern_key UNIQUE (organization_id, agent_id, pattern);
59
+ ALTER TABLE public.agent_users
60
+ ADD CONSTRAINT agent_users_pkey PRIMARY KEY (organization_id, agent_id, platform, user_id);
61
+ ALTER TABLE public.grants
62
+ ADD CONSTRAINT grants_pkey PRIMARY KEY (organization_id, agent_id, kind, pattern);
63
+
64
+ -- ── 5. Re-add composite FKs (organization_id, agent_id) → agents(organization_id, id).
65
+ ALTER TABLE public.agent_grants
66
+ ADD CONSTRAINT agent_grants_org_agent_fkey
67
+ FOREIGN KEY (organization_id, agent_id) REFERENCES public.agents(organization_id, id) ON DELETE CASCADE;
68
+ ALTER TABLE public.agent_connections
69
+ ADD CONSTRAINT agent_connections_org_agent_fkey
70
+ FOREIGN KEY (organization_id, agent_id) REFERENCES public.agents(organization_id, id) ON DELETE CASCADE;
71
+ ALTER TABLE public.agent_users
72
+ ADD CONSTRAINT agent_users_org_agent_fkey
73
+ FOREIGN KEY (organization_id, agent_id) REFERENCES public.agents(organization_id, id) ON DELETE CASCADE;
74
+ ALTER TABLE public.agent_channel_bindings
75
+ ADD CONSTRAINT agent_channel_bindings_org_agent_fkey
76
+ FOREIGN KEY (organization_id, agent_id) REFERENCES public.agents(organization_id, id) ON DELETE CASCADE;
77
+ ALTER TABLE public.grants
78
+ ADD CONSTRAINT grants_org_agent_fkey
79
+ FOREIGN KEY (organization_id, agent_id) REFERENCES public.agents(organization_id, id) ON DELETE CASCADE;
80
+ ALTER TABLE public.scheduled_jobs
81
+ ADD CONSTRAINT scheduled_jobs_org_agent_fkey
82
+ FOREIGN KEY (organization_id, created_by_agent) REFERENCES public.agents(organization_id, id) ON DELETE CASCADE;
83
+
84
+ -- migrate:down
85
+ -- Reverse the swap. NOTE: this WILL FAIL if two orgs ended up sharing an
86
+ -- agent ID after this migration shipped (the previous PK on (id) requires
87
+ -- global uniqueness). That's by design — this migration's whole purpose is
88
+ -- to allow per-org agent IDs that the old PK forbids.
89
+
90
+ ALTER TABLE public.scheduled_jobs DROP CONSTRAINT IF EXISTS scheduled_jobs_org_agent_fkey;
91
+ ALTER TABLE public.grants DROP CONSTRAINT IF EXISTS grants_org_agent_fkey;
92
+ ALTER TABLE public.agent_channel_bindings DROP CONSTRAINT IF EXISTS agent_channel_bindings_org_agent_fkey;
93
+ ALTER TABLE public.agent_users DROP CONSTRAINT IF EXISTS agent_users_org_agent_fkey;
94
+ ALTER TABLE public.agent_connections DROP CONSTRAINT IF EXISTS agent_connections_org_agent_fkey;
95
+ ALTER TABLE public.agent_grants DROP CONSTRAINT IF EXISTS agent_grants_org_agent_fkey;
96
+
97
+ ALTER TABLE public.grants DROP CONSTRAINT IF EXISTS grants_pkey;
98
+ ALTER TABLE public.agent_users DROP CONSTRAINT IF EXISTS agent_users_pkey;
99
+ ALTER TABLE public.agent_grants DROP CONSTRAINT IF EXISTS agent_grants_org_agent_pattern_key;
100
+
101
+ ALTER TABLE public.agents DROP CONSTRAINT IF EXISTS agents_pkey;
102
+ ALTER TABLE public.agents ADD CONSTRAINT agents_pkey PRIMARY KEY (id);
103
+
104
+ ALTER TABLE public.agent_grants ADD CONSTRAINT agent_grants_agent_id_pattern_key UNIQUE (agent_id, pattern);
105
+ ALTER TABLE public.agent_users ADD CONSTRAINT agent_users_pkey PRIMARY KEY (agent_id, platform, user_id);
106
+ ALTER TABLE public.grants ADD CONSTRAINT grants_pkey PRIMARY KEY (agent_id, kind, pattern);
107
+
108
+ ALTER TABLE public.agent_grants
109
+ ADD CONSTRAINT agent_grants_agent_id_fkey FOREIGN KEY (agent_id) REFERENCES public.agents(id) ON DELETE CASCADE;
110
+ ALTER TABLE public.agent_connections
111
+ ADD CONSTRAINT agent_connections_agent_id_fkey FOREIGN KEY (agent_id) REFERENCES public.agents(id) ON DELETE CASCADE;
112
+ ALTER TABLE public.agent_users
113
+ ADD CONSTRAINT agent_users_agent_id_fkey FOREIGN KEY (agent_id) REFERENCES public.agents(id) ON DELETE CASCADE;
114
+ ALTER TABLE public.agent_channel_bindings
115
+ ADD CONSTRAINT agent_channel_bindings_agent_id_fkey FOREIGN KEY (agent_id) REFERENCES public.agents(id) ON DELETE CASCADE;
116
+ ALTER TABLE public.grants
117
+ ADD CONSTRAINT grants_agent_id_fkey FOREIGN KEY (agent_id) REFERENCES public.agents(id) ON DELETE CASCADE;
118
+ ALTER TABLE public.scheduled_jobs
119
+ ADD CONSTRAINT scheduled_jobs_agent_fkey FOREIGN KEY (created_by_agent) REFERENCES public.agents(id) ON DELETE CASCADE;
120
+
121
+ ALTER TABLE public.grants ALTER COLUMN organization_id DROP NOT NULL;
122
+ ALTER TABLE public.agent_channel_bindings ALTER COLUMN organization_id DROP NOT NULL;
123
+ ALTER TABLE public.agent_users ALTER COLUMN organization_id DROP NOT NULL;
124
+ ALTER TABLE public.agent_connections ALTER COLUMN organization_id DROP NOT NULL;
125
+ ALTER TABLE public.agent_grants ALTER COLUMN organization_id DROP NOT NULL;
@@ -0,0 +1,134 @@
1
+ -- migrate:up
2
+
3
+ -- Materialize the fulltext search vector as a STORED column.
4
+ --
5
+ -- Why a real column instead of the previous expression-indexed `to_tsvector(payload_text)`:
6
+ -- 1. It includes both `title` (weight A) and `payload_text` (weight B) — the
7
+ -- same shape buildSearchDocumentExpr() in content-search.ts uses for
8
+ -- ranking. Retrieval (@@) and ts_rank_cd now read the same vector, so
9
+ -- title-only hits surface correctly and ranking doesn't recompute the
10
+ -- vector per matched row at query time.
11
+ -- 2. Planner-stable: `search_tsv @@ to_tsquery(...)` is a plain column
12
+ -- reference — no expression-shape matching, no aliasing risk where the
13
+ -- GIN gets skipped because the WHERE expression isn't byte-identical to
14
+ -- the indexed expression.
15
+ -- 3. The new GIN strictly subsumes the old payload-only one (same lexemes
16
+ -- plus title's), so we drop the old index and recover its write
17
+ -- amplification on every events insert.
18
+ --
19
+ -- Operational note: ADD COLUMN ... GENERATED STORED rewrites the events
20
+ -- table under ACCESS EXCLUSIVE. On a 1M-row table expect on the order of a
21
+ -- minute; run during a quiet window. CONCURRENTLY does not apply to ADD
22
+ -- COLUMN; it only applies to CREATE INDEX, which is a separate statement
23
+ -- below.
24
+
25
+ ALTER TABLE public.events
26
+ ADD COLUMN search_tsv tsvector GENERATED ALWAYS AS (
27
+ setweight(to_tsvector('english', COALESCE(title, '')), 'A') ||
28
+ setweight(to_tsvector('english', COALESCE(payload_text, '')), 'B')
29
+ ) STORED;
30
+
31
+ CREATE INDEX idx_events_search_tsv ON public.events USING gin (search_tsv);
32
+
33
+ DROP INDEX IF EXISTS public.idx_events_fulltext;
34
+
35
+ -- Recreate the current_event_records view so it exposes search_tsv to
36
+ -- callers that go through the supersession filter (the view materializes
37
+ -- its column list at create time, so `e.*`-style auto-pickup doesn't
38
+ -- apply). buildSearchDocumentExpr() reads `f.search_tsv` / `fi.search_tsv`
39
+ -- where f and fi are this view's aliases, so without this the view-backed
40
+ -- queries fail with "column ... does not exist".
41
+
42
+ DROP VIEW IF EXISTS public.current_event_records;
43
+ CREATE VIEW public.current_event_records AS
44
+ SELECT e.id,
45
+ e.organization_id,
46
+ e.entity_ids,
47
+ e.origin_id,
48
+ e.title,
49
+ e.payload_type,
50
+ e.payload_text,
51
+ e.payload_data,
52
+ e.payload_template,
53
+ e.attachments,
54
+ e.metadata,
55
+ e.score,
56
+ emb.embedding,
57
+ e.author_name,
58
+ e.source_url,
59
+ e.occurred_at,
60
+ e.created_at,
61
+ e.origin_parent_id,
62
+ COALESCE(length(e.payload_text), 0) AS content_length,
63
+ e.search_tsv,
64
+ e.origin_type,
65
+ e.connector_key,
66
+ e.connection_id,
67
+ e.feed_key,
68
+ e.feed_id,
69
+ e.run_id,
70
+ e.semantic_type,
71
+ e.client_id,
72
+ e.created_by,
73
+ e.interaction_type,
74
+ e.interaction_status,
75
+ e.interaction_input_schema,
76
+ e.interaction_input,
77
+ e.interaction_output,
78
+ e.interaction_error,
79
+ e.supersedes_event_id
80
+ FROM (public.events e
81
+ LEFT JOIN public.event_embeddings emb ON ((emb.event_id = e.id)))
82
+ WHERE (NOT (EXISTS ( SELECT 1
83
+ FROM public.events newer
84
+ WHERE (newer.supersedes_event_id = e.id))));
85
+
86
+ -- migrate:down
87
+
88
+ DROP VIEW IF EXISTS public.current_event_records;
89
+ CREATE VIEW public.current_event_records AS
90
+ SELECT e.id,
91
+ e.organization_id,
92
+ e.entity_ids,
93
+ e.origin_id,
94
+ e.title,
95
+ e.payload_type,
96
+ e.payload_text,
97
+ e.payload_data,
98
+ e.payload_template,
99
+ e.attachments,
100
+ e.metadata,
101
+ e.score,
102
+ emb.embedding,
103
+ e.author_name,
104
+ e.source_url,
105
+ e.occurred_at,
106
+ e.created_at,
107
+ e.origin_parent_id,
108
+ COALESCE(length(e.payload_text), 0) AS content_length,
109
+ e.origin_type,
110
+ e.connector_key,
111
+ e.connection_id,
112
+ e.feed_key,
113
+ e.feed_id,
114
+ e.run_id,
115
+ e.semantic_type,
116
+ e.client_id,
117
+ e.created_by,
118
+ e.interaction_type,
119
+ e.interaction_status,
120
+ e.interaction_input_schema,
121
+ e.interaction_input,
122
+ e.interaction_output,
123
+ e.interaction_error,
124
+ e.supersedes_event_id
125
+ FROM (public.events e
126
+ LEFT JOIN public.event_embeddings emb ON ((emb.event_id = e.id)))
127
+ WHERE (NOT (EXISTS ( SELECT 1
128
+ FROM public.events newer
129
+ WHERE (newer.supersedes_event_id = e.id))));
130
+
131
+ CREATE INDEX idx_events_fulltext ON public.events
132
+ USING gin (to_tsvector('english'::regconfig, COALESCE(payload_text, ''::text)));
133
+ DROP INDEX IF EXISTS public.idx_events_search_tsv;
134
+ ALTER TABLE public.events DROP COLUMN search_tsv;
@@ -0,0 +1,25 @@
1
+ -- migrate:up
2
+
3
+ -- Partial index for the dashboard's 14-day lifecycle-cumulative-stats query
4
+ -- (lifecycleCumulativeStatsSql in packages/owletto/src/lib/api/metric-series.ts).
5
+ --
6
+ -- The query reads events older than `now() - interval '14 days'` and
7
+ -- aggregates by date_trunc('day', created_at), with every COUNT FILTER
8
+ -- already gated on semantic_type='change' AND metadata->>'category'='lifecycle'.
9
+ -- A partial index whose predicate matches that gate prunes the scan from
10
+ -- "all events in the last 14 days for the org" to "just the lifecycle
11
+ -- changes" — typically a small slice of total events. The aggregation
12
+ -- itself is cheap once the row set is narrow.
13
+ --
14
+ -- Why not a materialized view: at current scale the bottleneck is the row
15
+ -- scan, not the per-bucket COUNT FILTER work. A partial index removes the
16
+ -- scan cost without introducing a refresh job or staleness. Revisit if the
17
+ -- post-prune row count grows into the millions.
18
+
19
+ CREATE INDEX idx_events_lifecycle_changes
20
+ ON public.events (organization_id, created_at)
21
+ WHERE semantic_type = 'change' AND metadata->>'category' = 'lifecycle';
22
+
23
+ -- migrate:down
24
+
25
+ DROP INDEX IF EXISTS public.idx_events_lifecycle_changes;
@@ -0,0 +1,49 @@
1
+ -- migrate:up
2
+
3
+ -- Drop 4 indexes that pg_stat_user_indexes reported `idx_scan = 0` after 28h
4
+ -- of prod uptime AND are not referenced from any active code path.
5
+ --
6
+ -- A larger set (4 more, ~5 GB combined) was originally bundled here but
7
+ -- review caught they're not actually unused — they're dormant. The three
8
+ -- big search indexes (`idx_events_embedding`, `idx_events_raw_content_trgm`,
9
+ -- `idx_events_search_tsv`) are explicitly used by the ANN/fulltext/trigram
10
+ -- branches of `approximate_candidate_search` in
11
+ -- `packages/server/src/utils/content-search.ts:1707-1733`. The `search()`
12
+ -- agent tool path threads through there; prod just hasn't called it in
13
+ -- 28h, but a single user-initiated search would now time out at 6s and
14
+ -- return empty results without those indexes (`content-search.ts:1850-1863`).
15
+ -- Similarly `idx_events_run_id` backs the "view in memory" filter
16
+ -- (`content-query-filters.ts:197-201`); rare, but a real path.
17
+ --
18
+ -- Keep those four until either (a) the dormant features are removed in
19
+ -- code, or (b) measured prod traffic confirms they're abandoned.
20
+ --
21
+ -- What remains is small but still real write amplification: each kept
22
+ -- INSERT into events updates these btrees. Combined size ~66 MB —
23
+ -- modest reclaim, but zero downside since the underlying queries don't
24
+ -- exist anywhere in the codebase today (verified by grep).
25
+ --
26
+ -- Plain `DROP INDEX` (not CONCURRENTLY) is used because dbmate's
27
+ -- `transaction:false` directive doesn't actually exit the transaction
28
+ -- block against the `pq` driver — see the comment in
29
+ -- 20260426130001_db_integrity_cleanup_concurrent.sql. These 4 indexes
30
+ -- are all small btrees so the ACCESS EXCLUSIVE on `events` during the
31
+ -- drop is sub-second; no operator runbook needed.
32
+
33
+ DROP INDEX IF EXISTS public.idx_events_entity_ids_occurred_at;
34
+ DROP INDEX IF EXISTS public.idx_events_origin_parent_id;
35
+ DROP INDEX IF EXISTS public.idx_events_thread_lookup;
36
+ DROP INDEX IF EXISTS public.idx_events_type;
37
+
38
+ -- migrate:down
39
+
40
+ CREATE INDEX IF NOT EXISTS idx_events_entity_ids_occurred_at
41
+ ON public.events USING btree ((entity_ids[1]), occurred_at DESC, id DESC)
42
+ WHERE ((entity_ids IS NOT NULL) AND (entity_ids <> '{}'::bigint[]));
43
+ CREATE INDEX IF NOT EXISTS idx_events_origin_parent_id
44
+ ON public.events USING btree (origin_parent_id);
45
+ CREATE INDEX IF NOT EXISTS idx_events_thread_lookup
46
+ ON public.events USING btree (origin_parent_id, occurred_at)
47
+ WHERE (origin_parent_id IS NOT NULL);
48
+ CREATE INDEX IF NOT EXISTS idx_events_type
49
+ ON public.events USING btree (origin_type) WHERE (origin_type IS NOT NULL);
@@ -0,0 +1,56 @@
1
+ -- migrate:up
2
+
3
+ -- Soft-delete feeds whose (connector_key, organization_id) has no active
4
+ -- connector_definition row.
5
+ --
6
+ -- Why: the 2026-05-16 audit found feeds 117-155 (and others) referencing
7
+ -- connector_key='website' in orgs that have no active definition for it
8
+ -- (only one org has `website` active; one definition is archived). Every
9
+ -- CheckDueFeeds tick (every minute) tried to materialize a sync run for
10
+ -- these feeds and threw "No active connector definition found for X." —
11
+ -- producing ~380 error logs / minute that masked real signal in stdout.
12
+ --
13
+ -- The app-side code path now warns + skips (no throw) for the same case
14
+ -- so future orphans don't spam logs either. This migration is the one-time
15
+ -- cleanup of the existing data.
16
+ --
17
+ -- Conservative criteria — match exactly the set CheckDueFeeds processes
18
+ -- (so we only soft-delete feeds that actually produce the error stream).
19
+ -- - feed has no pinned_version (= would have looked up connector_definitions)
20
+ -- - feed.deleted_at IS NULL (still considered active)
21
+ -- - feed.status = 'active' (CheckDueFeeds filters on this — see
22
+ -- packages/server/src/scheduled/check-due-feeds.ts:36-43)
23
+ -- - connection.deleted_at IS NULL AND connection.status = 'active' (same)
24
+ -- - NO active connector_definition exists for that (key, organization) pair
25
+ --
26
+ -- Feeds in paused / pending_auth / error / revoked states are left alone
27
+ -- — operators may be mid-recovery on them and they don't contribute to
28
+ -- the error spam (CheckDueFeeds skips them anyway).
29
+ --
30
+ -- The same feed remains recoverable: clearing `deleted_at` + reinstalling
31
+ -- the connector definition for the org restores it.
32
+
33
+ UPDATE public.feeds f
34
+ SET deleted_at = now()
35
+ FROM public.connections c
36
+ WHERE f.connection_id = c.id
37
+ AND f.deleted_at IS NULL
38
+ AND f.pinned_version IS NULL
39
+ AND f.status = 'active'
40
+ AND c.deleted_at IS NULL
41
+ AND c.status = 'active'
42
+ AND NOT EXISTS (
43
+ SELECT 1
44
+ FROM public.connector_definitions cd
45
+ WHERE cd.key = c.connector_key
46
+ AND cd.organization_id = f.organization_id
47
+ AND cd.status = 'active'
48
+ );
49
+
50
+ -- migrate:down
51
+
52
+ -- No-op: re-attaching the orphan feeds would require knowing which were
53
+ -- soft-deleted by this migration vs. by an operator action. The original
54
+ -- error condition is fixed in code; this migration is a one-shot data
55
+ -- cleanup. To recover specific feeds in prod, clear `deleted_at` on the
56
+ -- targeted rows manually and re-install the connector definition.