thinkwork-cli 0.13.1 → 1.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 (262) hide show
  1. package/README.md +3 -14
  2. package/dist/{api-client-4DZ266J5.js → api-client-4P4JZOBX.js} +3 -1
  3. package/dist/bowser-6Z6JFAIR.js +2822 -0
  4. package/dist/{chunk-Y2WHJECR.js → chunk-4OMDWYZQ.js} +5 -44
  5. package/dist/chunk-AD2NIWB5.js +6516 -0
  6. package/dist/chunk-DV3MRBR5.js +43 -0
  7. package/dist/chunk-EI5ZX7CN.js +522 -0
  8. package/dist/chunk-EUJ7X6YD.js +126 -0
  9. package/dist/chunk-GG5WKBOX.js +436 -0
  10. package/dist/chunk-I64EZSU5.js +358 -0
  11. package/dist/chunk-KMIJ5KGT.js +128 -0
  12. package/dist/chunk-MWQ7RO76.js +137 -0
  13. package/dist/chunk-OCPHU34C.js +996 -0
  14. package/dist/chunk-OLUQ4U2Z.js +1728 -0
  15. package/dist/chunk-XYM7PE2A.js +2264 -0
  16. package/dist/chunk-YQCYD6NE.js +3299 -0
  17. package/dist/cli.js +49156 -31257
  18. package/dist/commands/enterprise/templates/deploy-repo/scripts/apply-release.mjs +0 -1
  19. package/dist/commands/enterprise/templates/deploy-repo/scripts/smoke.mjs +0 -1
  20. package/dist/commands/enterprise/templates/deploy-repo/terraform/main.tf +0 -14
  21. package/dist/dist-es-2OTNTL4N.js +315 -0
  22. package/dist/dist-es-2XVAH4BQ.js +14726 -0
  23. package/dist/dist-es-3DPDUJAH.js +2000 -0
  24. package/dist/dist-es-BNZTBBVC.js +176 -0
  25. package/dist/dist-es-CD2RBDAC.js +88 -0
  26. package/dist/dist-es-I4FW57PS.js +88 -0
  27. package/dist/dist-es-KL7Y5NB2.js +377 -0
  28. package/dist/dist-es-KVFWJNDY.js +318 -0
  29. package/dist/dist-es-PS73DWNL.js +70 -0
  30. package/dist/dist-es-S3NZQFGT.js +170 -0
  31. package/dist/dist-es-SGCA4P32.js +48 -0
  32. package/dist/dist-es-WBM6J4RA.js +70 -0
  33. package/dist/dist-es-ZGVADYIF.js +485 -0
  34. package/dist/dist-es-ZKC2QKD2.js +377 -0
  35. package/dist/drizzle/0076_scheduled_jobs_marco_backfill.sql +2 -1
  36. package/dist/drizzle/0079_seed_tenant_customize_catalog.sql +1 -0
  37. package/dist/drizzle/0090_brain_schema_extraction.sql +10 -0
  38. package/dist/drizzle/0132_drop_computer_tables.sql +3 -0
  39. package/dist/drizzle/0160_compliance_event_types_plugins.sql +2 -0
  40. package/dist/drizzle/0214_think137_drop_judge_evidence_roi.sql +1 -0
  41. package/dist/drizzle/0216_drop_agent_loop_versions_goal_worker_policy.sql +1 -0
  42. package/dist/drizzle/0227_analyst_reader_role.sql +17 -20
  43. package/dist/drizzle/0230_analyst_rls.sql +28 -36
  44. package/dist/drizzle/0250_kg_schema_extraction.sql +27 -0
  45. package/dist/drizzle/0251_brain_stragglers.sql +21 -0
  46. package/dist/drizzle/0253_drop_brain_substrate.sql +2 -0
  47. package/dist/drizzle/0257_agents_runtime_allow_harness.sql +24 -0
  48. package/dist/drizzle/0261_native_auth_control_plane.sql +332 -0
  49. package/dist/drizzle/0262_auth_subscription_tickets.sql +59 -0
  50. package/dist/drizzle/0263_agentcore_runtime_identifier.sql +69 -0
  51. package/dist/drizzle/0263_drop_workos_auth_runtime.sql +236 -0
  52. package/dist/drizzle/0264_auth_enrollment_attempt_limits.sql +15 -0
  53. package/dist/drizzle/0265_eval_runs_requester_identity.sql +28 -0
  54. package/dist/drizzle/0265_require_fully_valid_auth_publication.sql +27 -0
  55. package/dist/drizzle/0266_auth_identity_recovery_grants.sql +9 -0
  56. package/dist/drizzle/0266_eval_profiles_runtime_type.sql +12 -0
  57. package/dist/drizzle/0267_ontology_candidate_rejections.sql +53 -0
  58. package/dist/drizzle/0268_identity_crosswalk_routing.sql +182 -0
  59. package/dist/drizzle/0269_ontology_system_map.sql +31 -0
  60. package/dist/drizzle/0270_tool_execution_ledger.sql +115 -0
  61. package/dist/drizzle/0271_ontology_twin_declarations.sql +47 -0
  62. package/dist/drizzle/0272_identity_graph_projection_cursor.sql +23 -0
  63. package/dist/drizzle/0273_twin_materialization_suggestions.sql +29 -0
  64. package/dist/drizzle/0274_tenant_mcp_twin_keys.sql +59 -0
  65. package/dist/drizzle/0275_identity_bulk_rebuild_fence.sql +30 -0
  66. package/dist/drizzle/0276_tool_execution_ledger_allow_cascade_delete.sql +18 -0
  67. package/dist/drizzle/0277_knowledge_base_sources.sql +107 -0
  68. package/dist/drizzle/0278_kb_page_transcription.sql +55 -0
  69. package/dist/drizzle/0279_mcp_servers_off_plugin_provenance.sql +93 -0
  70. package/dist/drizzle/0280_work_item_external_refs_drop_plane_provider.sql +46 -0
  71. package/dist/drizzle/0281_tenant_mcp_twin_keys_expiry_suffix.sql +9 -0
  72. package/dist/drizzle/0282_tenant_mcp_twin_keys_grants.sql +24 -0
  73. package/dist/drizzle/0283_agents_agentcore_runtime_dispatch.sql +13 -0
  74. package/dist/drizzle/0284_user_brain_claims.sql +69 -0
  75. package/dist/drizzle/0285_tenant_mcp_twin_keys_trusted_subsystem.sql +30 -0
  76. package/dist/drizzle/0286_brain_analytics_key.sql +26 -0
  77. package/dist/drizzle/0287_brain_subject.sql +25 -0
  78. package/dist/drizzle/0288_multi_lane_identities.sql +25 -0
  79. package/dist/event-streams-7JNLJEEL.js +894 -0
  80. package/dist/event-streams-KDF5RWR6.js +1515 -0
  81. package/dist/loadSso-ETCXTR4I.js +576 -0
  82. package/dist/loadSso-UBD7KUHE.js +583 -0
  83. package/dist/plugins/README.md +2 -2
  84. package/dist/plugins/catalog/package.json +1 -5
  85. package/dist/plugins/catalog/src/__tests__/build-catalog.test.ts +6 -4
  86. package/dist/plugins/catalog/src/__tests__/catalog.test.ts +21 -21
  87. package/dist/plugins/catalog/src/__tests__/contracts.test.ts +102 -94
  88. package/dist/plugins/catalog/src/__tests__/plugin-package.test.ts +7 -8
  89. package/dist/plugins/catalog/src/__tests__/plugin-registry.test.ts +0 -20
  90. package/dist/plugins/catalog/src/contracts.ts +17 -179
  91. package/dist/plugins/catalog/src/registry/generated-first-party.ts +0 -24
  92. package/dist/plugins/n8n/test/integrated-app-contract.test.ts +1 -1
  93. package/dist/plugins/twenty/package.json +2 -1
  94. package/dist/plugins/twenty/scripts/LAMBDA_DEPLOY.md +114 -0
  95. package/dist/plugins/twenty/scripts/build-lambda.sh +29 -0
  96. package/dist/plugins/twenty/scripts/configure-product-views.ts +195 -0
  97. package/dist/plugins/twenty/scripts/lambda-handler.ts +49 -0
  98. package/dist/plugins/twenty/scripts/lib/__tests__/load-attachments.test.ts +38 -0
  99. package/dist/plugins/twenty/scripts/lib/__tests__/load-records.test.ts +145 -2
  100. package/dist/plugins/twenty/scripts/lib/__tests__/mappers.test.ts +397 -151
  101. package/dist/plugins/twenty/scripts/lib/__tests__/members-ensure.test.ts +28 -129
  102. package/dist/plugins/twenty/scripts/lib/__tests__/provision-members-db.test.ts +10 -0
  103. package/dist/plugins/twenty/scripts/lib/__tests__/schema-ensure.test.ts +13 -8
  104. package/dist/plugins/twenty/scripts/lib/lastmile-reader.ts +222 -2
  105. package/dist/plugins/twenty/scripts/lib/load-attachments.ts +29 -0
  106. package/dist/plugins/twenty/scripts/lib/load-records.ts +160 -43
  107. package/dist/plugins/twenty/scripts/lib/mappers.ts +368 -152
  108. package/dist/plugins/twenty/scripts/lib/members-ensure.ts +43 -268
  109. package/dist/plugins/twenty/scripts/lib/provision-members-db.ts +57 -0
  110. package/dist/plugins/twenty/scripts/lib/schema-ensure.ts +136 -31
  111. package/dist/plugins/twenty/scripts/migrate-lastmile.ts +254 -152
  112. package/dist/plugins/twenty/scripts/provision-twenty-members.ts +44 -1
  113. package/dist/plugins/twenty/scripts/purge-lastmile-import.ts +178 -0
  114. package/dist/plugins/twenty/scripts/restructure-products.ts +312 -0
  115. package/dist/plugins/twenty/scripts/retag-stage-values.ts +362 -0
  116. package/dist/plugins/twenty/test/manifest.test.ts +2 -4
  117. package/dist/scripts/post-deploy.sh +79 -1
  118. package/dist/signin-5QZNRSOO.js +821 -0
  119. package/dist/sso-oidc-XK673P54.js +837 -0
  120. package/dist/sts-OXI2UALF.js +1113 -0
  121. package/dist/sts-SQW4NBK3.js +1117 -0
  122. package/dist/terraform/examples/greenfield/main.tf +391 -176
  123. package/dist/terraform/examples/greenfield/terraform.tfvars.example +26 -8
  124. package/dist/terraform/modules/app/agentcore-admin/main.tf +47 -0
  125. package/dist/terraform/modules/app/agentcore-code-interpreter/README.md +39 -47
  126. package/dist/terraform/modules/app/agentcore-code-interpreter/main.tf +13 -66
  127. package/dist/terraform/modules/app/agentcore-identity/main.tf +253 -0
  128. package/dist/terraform/modules/app/agentcore-identity/outputs.tf +51 -0
  129. package/dist/terraform/modules/app/agentcore-identity/scripts/bootstrap_twenty_oauth_client.sh +239 -0
  130. package/dist/terraform/modules/app/agentcore-identity/scripts/delete_identity.sh +22 -0
  131. package/dist/terraform/modules/app/agentcore-identity/scripts/delete_twenty_identity.sh +16 -0
  132. package/dist/terraform/modules/app/agentcore-identity/scripts/delete_workload_identity.sh +19 -0
  133. package/dist/terraform/modules/app/agentcore-identity/scripts/ensure_workload_identity.sh +29 -0
  134. package/dist/terraform/modules/app/agentcore-identity/scripts/read_identity.sh +55 -0
  135. package/dist/terraform/modules/app/agentcore-identity/scripts/reconcile_identity.sh +73 -0
  136. package/dist/terraform/modules/app/agentcore-identity/scripts/reconcile_twenty_identity.sh +45 -0
  137. package/dist/terraform/modules/app/agentcore-identity/scripts/reconcile_twenty_provider.mjs +123 -0
  138. package/dist/terraform/modules/app/agentcore-identity/tests/tainted-marker/after.tf +15 -0
  139. package/dist/terraform/modules/app/agentcore-identity/tests/tainted-marker/before.tf +7 -0
  140. package/dist/terraform/modules/app/agentcore-identity/tests/tainted-marker/identity-after.tf +8 -0
  141. package/dist/terraform/modules/app/agentcore-identity/tests/tainted-marker/identity-before.tf +9 -0
  142. package/dist/terraform/modules/app/agentcore-identity/tests/tainted-marker/thinkwork-after.tf +3 -0
  143. package/dist/terraform/modules/app/agentcore-identity/tests/tainted-marker/thinkwork-before.tf +3 -0
  144. package/dist/terraform/modules/app/agentcore-identity/variables.tf +60 -0
  145. package/dist/terraform/modules/app/agentcore-memory/README.md +46 -9
  146. package/dist/terraform/modules/app/agentcore-memory/main.tf +27 -10
  147. package/dist/terraform/modules/app/agentcore-memory/scripts/create_or_find_memory.sh +156 -24
  148. package/dist/terraform/modules/app/agentcore-pi/README.md +1 -3
  149. package/dist/terraform/modules/app/agentcore-pi/main.tf +42 -52
  150. package/dist/terraform/modules/app/agentcore-pi/outputs.tf +3 -3
  151. package/dist/terraform/modules/app/agentcore-pi/scripts/reconcile_pi_runtime.mjs +175 -0
  152. package/dist/terraform/modules/app/agentcore-pi/variables.tf +35 -28
  153. package/dist/terraform/modules/app/agentcore-platform/main.tf +68 -0
  154. package/dist/terraform/modules/app/agentcore-runtime/main.tf +4 -20
  155. package/dist/terraform/modules/app/appsync-subscriptions/main.tf +127 -21
  156. package/dist/terraform/modules/app/appsync-subscriptions/outputs.tf +0 -6
  157. package/dist/terraform/modules/app/appsync-subscriptions/variables.tf +5 -0
  158. package/dist/terraform/modules/app/capability-broker/main.tf +2 -2
  159. package/dist/terraform/modules/app/capability-broker/variables.tf +11 -0
  160. package/dist/terraform/modules/app/deployment-control-plane/README.md +264 -0
  161. package/dist/terraform/modules/app/deployment-control-plane/buildspec.yml +4 -1
  162. package/dist/terraform/modules/app/deployment-control-plane/main.tf +94 -0
  163. package/dist/terraform/modules/app/deployment-control-plane/outputs.tf +5 -0
  164. package/dist/terraform/modules/app/deployment-control-plane/runner.py +3689 -233
  165. package/dist/terraform/modules/app/deployment-control-plane/test_runner_bundle.py +2579 -140
  166. package/dist/terraform/modules/app/deployment-control-plane/variables.tf +33 -0
  167. package/dist/terraform/modules/app/lambda-api/auth-state.tf +72 -0
  168. package/dist/terraform/modules/app/lambda-api/chat-latency-observability.tf +466 -0
  169. package/dist/terraform/modules/app/lambda-api/dispatch-dlq.tf +55 -0
  170. package/dist/terraform/modules/app/lambda-api/handlers.tf +531 -619
  171. package/dist/terraform/modules/app/lambda-api/iam-grouped.tf +350 -241
  172. package/dist/terraform/modules/app/lambda-api/main.tf +75 -13
  173. package/dist/terraform/modules/app/lambda-api/memory-alarms.tf +3 -28
  174. package/dist/terraform/modules/app/lambda-api/oauth-secrets.tf +1 -2
  175. package/dist/terraform/modules/app/lambda-api/outputs.tf +17 -10
  176. package/dist/terraform/modules/app/lambda-api/runtime-config.tf +3 -21
  177. package/dist/terraform/modules/app/lambda-api/turn-assertion.tf +45 -0
  178. package/dist/terraform/modules/app/lambda-api/variables.tf +307 -121
  179. package/dist/{plugins/n8n/terraform → terraform/modules/app}/n8n/README.md +1 -1
  180. package/dist/{plugins/n8n/terraform → terraform/modules/app}/n8n/main.tf +18 -18
  181. package/dist/terraform/modules/app/sandbox-log-scrubber/README.md +11 -10
  182. package/dist/terraform/modules/app/sandbox-log-scrubber/main.tf +5 -5
  183. package/dist/terraform/modules/app/static-site/main.tf +1 -1
  184. package/dist/terraform/modules/app/www-dns/main.tf +9 -27
  185. package/dist/terraform/modules/app/www-dns/variables.tf +4 -10
  186. package/dist/terraform/modules/data/aurora-postgres/README.md +34 -0
  187. package/dist/terraform/modules/data/aurora-postgres/alarms.tf +82 -0
  188. package/dist/terraform/modules/data/aurora-postgres/main.tf +27 -57
  189. package/dist/terraform/modules/data/aurora-postgres/outputs.tf +0 -9
  190. package/dist/terraform/modules/data/aurora-postgres/variables.tf +41 -0
  191. package/dist/terraform/modules/data/s3-buckets/main.tf +5 -6
  192. package/dist/terraform/modules/foundation/cognito/main.tf +367 -72
  193. package/dist/terraform/modules/foundation/cognito/outputs.tf +47 -0
  194. package/dist/terraform/modules/foundation/cognito/variables.tf +131 -9
  195. package/dist/terraform/modules/thinkwork/README.md +5 -1
  196. package/dist/terraform/modules/thinkwork/main.tf +459 -234
  197. package/dist/terraform/modules/thinkwork/outputs.tf +44 -62
  198. package/dist/terraform/modules/thinkwork/variables.tf +353 -89
  199. package/dist/terraform/schema.graphql +69 -36
  200. package/dist/workspace-defaults/files/INSTRUCTIONS.md +9 -11
  201. package/dist/workspace-defaults/files/MEMORY_GUIDE.md +38 -35
  202. package/dist/workspace-defaults/files/ROUTER.md +1 -1
  203. package/dist/workspace-defaults/files/TOOLS.md +9 -39
  204. package/dist/workspace-defaults/files/USER.md +1 -0
  205. package/dist/workspace-defaults/files/agents/analyst/INSTRUCTIONS.md +6 -3
  206. package/dist/workspace-defaults/files/catalog-skills/brain-brief-builder/SKILL.md +165 -0
  207. package/dist/workspace-defaults/files/catalog-skills/n8n-workflow-operator/SKILL.md +75 -0
  208. package/dist/workspace-defaults/files/catalog-skills/n8n-workflow-operator/references/mcp-tooling.md +40 -0
  209. package/dist/workspace-defaults/files/catalog-skills/n8n-workflow-operator/references/validation-and-handoff.md +40 -0
  210. package/dist/workspace-defaults/files/catalog-skills/n8n-workflow-operator/references/workflow-authoring.md +46 -0
  211. package/dist/workspace-defaults/files/skills/artifact-builder/references/crm-dashboard.md +1 -1
  212. package/dist/workspace-defaults/files/skills/document-composer/SKILL.md +22 -3
  213. package/package.json +3 -2
  214. package/dist/drizzle/0261_thread_turns_origin_turn_index.sql +0 -11
  215. package/dist/plugins/company-data/README.md +0 -49
  216. package/dist/plugins/company-data/package.json +0 -21
  217. package/dist/plugins/company-data/src/index.ts +0 -30
  218. package/dist/plugins/company-data/src/manifest.ts +0 -26
  219. package/dist/plugins/company-data/test/manifest.test.ts +0 -116
  220. package/dist/plugins/company-data/tsconfig.json +0 -8
  221. package/dist/plugins/company-etl/README.md +0 -46
  222. package/dist/plugins/company-etl/package.json +0 -21
  223. package/dist/plugins/company-etl/src/index.ts +0 -29
  224. package/dist/plugins/company-etl/src/manifest.ts +0 -26
  225. package/dist/plugins/company-etl/test/manifest.test.ts +0 -108
  226. package/dist/plugins/company-etl/tsconfig.json +0 -8
  227. package/dist/plugins/lastmile/README.md +0 -50
  228. package/dist/plugins/lastmile/package.json +0 -23
  229. package/dist/plugins/lastmile/smoke/lastmile-plugin-smoke.mjs +0 -661
  230. package/dist/plugins/lastmile/src/api/tasks-adapter.ts +0 -572
  231. package/dist/plugins/lastmile/src/discovery.fixture.ts +0 -64
  232. package/dist/plugins/lastmile/src/index.ts +0 -46
  233. package/dist/plugins/lastmile/src/manifest.ts +0 -139
  234. package/dist/plugins/lastmile/test/api/tasks-adapter.test.ts +0 -266
  235. package/dist/plugins/lastmile/test/discovery.test.ts +0 -88
  236. package/dist/plugins/lastmile/tsconfig.json +0 -8
  237. package/dist/plugins/n8n/smoke/n8n-managed-app-smoke.mjs +0 -409
  238. package/dist/plugins/twenty/smoke/twenty-managed-app-smoke.mjs +0 -359
  239. package/dist/plugins/workos-auth/package.json +0 -22
  240. package/dist/plugins/workos-auth/src/index.ts +0 -30
  241. package/dist/plugins/workos-auth/src/manifest.ts +0 -40
  242. package/dist/plugins/workos-auth/src/provider-contract.ts +0 -39
  243. package/dist/plugins/workos-auth/test/manifest.test.ts +0 -94
  244. package/dist/plugins/workos-auth/tsconfig.json +0 -8
  245. package/dist/terraform/modules/app/agentcore-code-interpreter/Dockerfile.sandbox-base +0 -61
  246. package/dist/terraform/modules/app/agentcore-code-interpreter/sandbox/sitecustomize.py +0 -227
  247. package/dist/terraform/modules/app/agentcore-code-interpreter/sandbox/test_sitecustomize.py +0 -277
  248. package/dist/terraform/modules/app/agentcore-code-interpreter/scripts/build_and_push_sandbox_base.sh +0 -70
  249. package/dist/terraform/modules/app/agentcore-harness/main.tf +0 -101
  250. package/dist/terraform/modules/app/agentcore-harness/outputs.tf +0 -9
  251. package/dist/terraform/modules/app/agentcore-harness/variables.tf +0 -25
  252. package/dist/terraform/modules/app/hindsight-memory/README.md +0 -89
  253. package/dist/terraform/modules/app/hindsight-memory/main.tf +0 -411
  254. package/dist/terraform/modules/data/bedrock-knowledge-base/main.tf +0 -102
  255. /package/dist/{plugins/n8n/terraform → terraform/modules/app}/n8n/outputs.tf +0 -0
  256. /package/dist/{plugins/n8n/terraform → terraform/modules/app}/n8n/scripts/sync-database.py +0 -0
  257. /package/dist/{plugins/n8n/terraform → terraform/modules/app}/n8n/scripts/test_sync_database.py +0 -0
  258. /package/dist/{plugins/n8n/terraform → terraform/modules/app}/n8n/variables.tf +0 -0
  259. /package/dist/{plugins/twenty/terraform → terraform/modules/app}/twenty/README.md +0 -0
  260. /package/dist/{plugins/twenty/terraform → terraform/modules/app}/twenty/main.tf +0 -0
  261. /package/dist/{plugins/twenty/terraform → terraform/modules/app}/twenty/outputs.tf +0 -0
  262. /package/dist/{plugins/twenty/terraform → terraform/modules/app}/twenty/variables.tf +0 -0
@@ -8,7 +8,6 @@
8
8
  * - `skills` — bundled skill folders seeded into the tenant catalog
9
9
  * - `infrastructure` — a managed-app Terraform deployment
10
10
  * - `ui-surface` — declared or launchable UI identity
11
- * - `auth-provider` — declared/admin-configured login federation capability
12
11
  *
13
12
  * Capability declarations are catalog metadata, not install lifecycle
14
13
  * components. They let a plugin advertise channel contracts whose runtime is
@@ -34,7 +33,6 @@ export const PLUGIN_COMPONENT_TYPES = [
34
33
  "skills",
35
34
  "infrastructure",
36
35
  "ui-surface",
37
- "auth-provider",
38
36
  ] as const;
39
37
 
40
38
  export type PluginComponentType = (typeof PLUGIN_COMPONENT_TYPES)[number];
@@ -212,6 +210,15 @@ export interface McpServerComponent {
212
210
  resultTransforms?: McpResultTransform[];
213
211
  /** Optional human notes about the tools the server exposes. */
214
212
  toolNotes?: string[];
213
+ /**
214
+ * THINK-623 — opt in to the long-call profile for this server's tools.
215
+ * Set it only when the server streams MCP progress notifications during
216
+ * long calls: the runtime then resets the per-attempt `callTool` wall on
217
+ * every notification (bounded by an absolute total ceiling) instead of
218
+ * killing the call at the fixed 60s wall. Servers that go quiet for
219
+ * longer than the per-attempt wall still time out.
220
+ */
221
+ longRunning?: boolean;
215
222
  }
216
223
 
217
224
  export interface SkillSupportingFile {
@@ -294,59 +301,11 @@ export interface UiSurfaceLaunchMetadata {
294
301
  entitlementProductKey?: string;
295
302
  }
296
303
 
297
- export const AUTH_PROVIDER_KEYS = ["workos"] as const;
298
-
299
- export type AuthProviderKey = (typeof AUTH_PROVIDER_KEYS)[number];
300
-
301
- export const AUTH_PROVIDER_CONFIG_FIELD_STORAGE = [
302
- "metadata",
303
- "secret-ref",
304
- ] as const;
305
-
306
- export type AuthProviderConfigFieldStorage =
307
- (typeof AUTH_PROVIDER_CONFIG_FIELD_STORAGE)[number];
308
-
309
- export interface AuthProviderConfigField {
310
- /** Operator-facing input key; values are never present in the manifest. */
311
- key: string;
312
- displayName: string;
313
- required: boolean;
314
- /**
315
- * metadata: non-secret config such as issuer/client id.
316
- * secret-ref: stored only as an operator-managed secret reference.
317
- */
318
- storage: AuthProviderConfigFieldStorage;
319
- }
320
-
321
- export interface AuthProviderPublicOption {
322
- /**
323
- * Public-safe option key. For the U1-approved fallback this is `sso`; do not
324
- * use provider-specific Google/Microsoft keys unless routing evidence exists.
325
- */
326
- key: string;
327
- displayName: string;
328
- providerSpecific: boolean;
329
- recommended?: boolean;
330
- }
331
-
332
- export interface AuthProviderComponent {
333
- type: "auth-provider";
334
- key: string;
335
- displayName: string;
336
- provider: AuthProviderKey;
337
- settingsSurface: string;
338
- /** Cognito IdP name the validated bridge will use; not a secret. */
339
- cognitoIdentityProviderName: string;
340
- configFields: AuthProviderConfigField[];
341
- publicOptions: AuthProviderPublicOption[];
342
- }
343
-
344
304
  export type PluginComponent =
345
305
  | McpServerComponent
346
306
  | SkillsComponent
347
307
  | InfrastructureComponent
348
- | UiSurfaceComponent
349
- | AuthProviderComponent;
308
+ | UiSurfaceComponent;
350
309
 
351
310
  export interface EmailChannelProviderOption {
352
311
  key: EmailChannelProviderKey;
@@ -566,12 +525,6 @@ function validatePluginVersion(value: unknown, pluginKey: string): void {
566
525
  case "ui-surface":
567
526
  validateUiSurfaceComponent(component as UiSurfaceComponent, label);
568
527
  break;
569
- case "auth-provider":
570
- validateAuthProviderComponent(
571
- component as AuthProviderComponent,
572
- label,
573
- );
574
- break;
575
528
  }
576
529
  }
577
530
 
@@ -727,6 +680,12 @@ function validateMcpServerComponent(
727
680
  );
728
681
  }
729
682
  }
683
+ if (
684
+ component.longRunning !== undefined &&
685
+ typeof component.longRunning !== "boolean"
686
+ ) {
687
+ throw new PluginManifestError(`${prefix}.longRunning must be a boolean`);
688
+ }
730
689
  const auth = component.auth as Partial<McpServerAuth> | undefined;
731
690
  if (!auth || typeof auth !== "object" || Array.isArray(auth)) {
732
691
  throw new PluginManifestError(`${prefix}.auth is required`);
@@ -1443,131 +1402,10 @@ function validateUiSurfaceLaunchMetadata(
1443
1402
  }
1444
1403
  }
1445
1404
 
1446
- function validateAuthProviderComponent(
1447
- component: Partial<AuthProviderComponent>,
1448
- label: string,
1449
- ): void {
1450
- const prefix = `${label}: auth-provider "${component.key}"`;
1451
- requireString(component.displayName, `${prefix}.displayName`);
1452
- if (
1453
- typeof component.provider !== "string" ||
1454
- !(AUTH_PROVIDER_KEYS as readonly string[]).includes(component.provider)
1455
- ) {
1456
- throw new PluginManifestError(
1457
- `${prefix}.provider "${String(component.provider)}" is not supported`,
1458
- );
1459
- }
1460
- requireString(component.settingsSurface, `${prefix}.settingsSurface`);
1461
- requireString(
1462
- component.cognitoIdentityProviderName,
1463
- `${prefix}.cognitoIdentityProviderName`,
1464
- );
1465
-
1466
- if (
1467
- !Array.isArray(component.configFields) ||
1468
- component.configFields.length === 0
1469
- ) {
1470
- throw new PluginManifestError(
1471
- `${prefix}.configFields must be a non-empty array`,
1472
- );
1473
- }
1474
- const seenConfigKeys = new Set<string>();
1475
- for (const [index, field] of component.configFields.entries()) {
1476
- const fieldPrefix = `${prefix}.configFields[${index}]`;
1477
- if (!field || typeof field !== "object" || Array.isArray(field)) {
1478
- throw new PluginManifestError(`${fieldPrefix} must be an object`);
1479
- }
1480
- rejectManifestValueCarrier(
1481
- field as unknown as Record<string, unknown>,
1482
- fieldPrefix,
1483
- );
1484
- requireString(field.key, `${fieldPrefix}.key`);
1485
- if (!CREDENTIAL_KEY_RE.test(field.key)) {
1486
- throw new PluginManifestError(
1487
- `${fieldPrefix}.key "${field.key}" must match ${CREDENTIAL_KEY_RE.source}`,
1488
- );
1489
- }
1490
- if (seenConfigKeys.has(field.key)) {
1491
- throw new PluginManifestError(
1492
- `${prefix}.configFields declares duplicate key "${field.key}"`,
1493
- );
1494
- }
1495
- seenConfigKeys.add(field.key);
1496
- requireString(field.displayName, `${fieldPrefix}.displayName`);
1497
- if (typeof field.required !== "boolean") {
1498
- throw new PluginManifestError(
1499
- `${fieldPrefix}.required must be a boolean`,
1500
- );
1501
- }
1502
- if (
1503
- !(AUTH_PROVIDER_CONFIG_FIELD_STORAGE as readonly string[]).includes(
1504
- field.storage,
1505
- )
1506
- ) {
1507
- throw new PluginManifestError(
1508
- `${fieldPrefix}.storage must be one of ${AUTH_PROVIDER_CONFIG_FIELD_STORAGE.join(", ")}`,
1509
- );
1510
- }
1511
- }
1512
-
1513
- if (
1514
- !Array.isArray(component.publicOptions) ||
1515
- component.publicOptions.length === 0
1516
- ) {
1517
- throw new PluginManifestError(
1518
- `${prefix}.publicOptions must be a non-empty array`,
1519
- );
1520
- }
1521
- const seenOptionKeys = new Set<string>();
1522
- let recommendedCount = 0;
1523
- for (const [index, option] of component.publicOptions.entries()) {
1524
- const optionPrefix = `${prefix}.publicOptions[${index}]`;
1525
- if (!option || typeof option !== "object" || Array.isArray(option)) {
1526
- throw new PluginManifestError(`${optionPrefix} must be an object`);
1527
- }
1528
- rejectManifestValueCarrier(
1529
- option as unknown as Record<string, unknown>,
1530
- optionPrefix,
1531
- );
1532
- requireString(option.key, `${optionPrefix}.key`);
1533
- if (!SLUG_RE.test(option.key)) {
1534
- throw new PluginManifestError(
1535
- `${optionPrefix}.key "${option.key}" must match ${SLUG_RE.source}`,
1536
- );
1537
- }
1538
- if (seenOptionKeys.has(option.key)) {
1539
- throw new PluginManifestError(
1540
- `${prefix}.publicOptions declares duplicate key "${option.key}"`,
1541
- );
1542
- }
1543
- seenOptionKeys.add(option.key);
1544
- requireString(option.displayName, `${optionPrefix}.displayName`);
1545
- if (typeof option.providerSpecific !== "boolean") {
1546
- throw new PluginManifestError(
1547
- `${optionPrefix}.providerSpecific must be a boolean`,
1548
- );
1549
- }
1550
- if (
1551
- option.recommended !== undefined &&
1552
- typeof option.recommended !== "boolean"
1553
- ) {
1554
- throw new PluginManifestError(
1555
- `${optionPrefix}.recommended must be a boolean`,
1556
- );
1557
- }
1558
- if (option.recommended === true) recommendedCount += 1;
1559
- }
1560
- if (recommendedCount !== 1) {
1561
- throw new PluginManifestError(
1562
- `${prefix}.publicOptions must declare exactly one recommended option`,
1563
- );
1564
- }
1565
- }
1566
-
1567
1405
  function rejectManifestValueCarrier(
1568
1406
  value: Record<string, unknown>,
1569
1407
  prefix: string,
1570
- context = "auth-provider manifests",
1408
+ context = "plugin manifests",
1571
1409
  ): void {
1572
1410
  for (const key of ["value", "secret", "defaultValue", "currentValue"]) {
1573
1411
  if (Object.prototype.hasOwnProperty.call(value, key)) {
@@ -3,27 +3,14 @@
3
3
  // pnpm --filter @thinkwork/plugin-catalog generate:plugins
4
4
 
5
5
  import { defineFirstPartyPluginPackage } from "../plugin-package";
6
- import { companyDataPluginPackage as rawCompanyDataPluginPackage } from "@thinkwork/plugin-company-data";
7
- import { companyEtlPluginPackage as rawCompanyEtlPluginPackage } from "@thinkwork/plugin-company-etl";
8
6
  import { emailChannelPluginPackage as rawEmailChannelPluginPackage } from "@thinkwork/plugin-email-channel";
9
- import { lastmilePluginPackage as rawLastmilePluginPackage } from "@thinkwork/plugin-lastmile";
10
7
  import { n8nPluginPackage as rawN8nPluginPackage } from "@thinkwork/plugin-n8n";
11
8
  import { sendgridPluginPackage as rawSendgridPluginPackage } from "@thinkwork/plugin-sendgrid";
12
9
  import { twentyPluginPackage as rawTwentyPluginPackage } from "@thinkwork/plugin-twenty";
13
- import { workosAuthPluginPackage as rawWorkosAuthPluginPackage } from "@thinkwork/plugin-workos-auth";
14
10
 
15
- export const companyDataPluginPackage = defineFirstPartyPluginPackage(
16
- rawCompanyDataPluginPackage,
17
- );
18
- export const companyEtlPluginPackage = defineFirstPartyPluginPackage(
19
- rawCompanyEtlPluginPackage,
20
- );
21
11
  export const emailChannelPluginPackage = defineFirstPartyPluginPackage(
22
12
  rawEmailChannelPluginPackage,
23
13
  );
24
- export const lastmilePluginPackage = defineFirstPartyPluginPackage(
25
- rawLastmilePluginPackage,
26
- );
27
14
  export const n8nPluginPackage = defineFirstPartyPluginPackage(
28
15
  rawN8nPluginPackage,
29
16
  );
@@ -33,28 +20,17 @@ export const sendgridPluginPackage = defineFirstPartyPluginPackage(
33
20
  export const twentyPluginPackage = defineFirstPartyPluginPackage(
34
21
  rawTwentyPluginPackage,
35
22
  );
36
- export const workosAuthPluginPackage = defineFirstPartyPluginPackage(
37
- rawWorkosAuthPluginPackage,
38
- );
39
23
 
40
- export const companyDataManifest = companyDataPluginPackage.manifest;
41
- export const companyEtlManifest = companyEtlPluginPackage.manifest;
42
24
  export const emailChannelManifest = emailChannelPluginPackage.manifest;
43
- export const lastmileManifest = lastmilePluginPackage.manifest;
44
25
  export const n8nManifest = n8nPluginPackage.manifest;
45
26
  export const sendgridManifest = sendgridPluginPackage.manifest;
46
27
  export const twentyManifest = twentyPluginPackage.manifest;
47
- export const workosAuthManifest = workosAuthPluginPackage.manifest;
48
28
 
49
29
  export const firstPartyPluginPackages = [
50
- companyDataPluginPackage,
51
- companyEtlPluginPackage,
52
30
  emailChannelPluginPackage,
53
- lastmilePluginPackage,
54
31
  n8nPluginPackage,
55
32
  sendgridPluginPackage,
56
33
  twentyPluginPackage,
57
- workosAuthPluginPackage,
58
34
  ] as const;
59
35
 
60
36
  /**
@@ -211,7 +211,7 @@ describe("n8n integrated app contract", () => {
211
211
  const docsPage = await readFile(
212
212
  fileURLToPath(
213
213
  new URL(
214
- "../../../docs/src/content/docs/applications/n8n.mdx",
214
+ "../../../docs/reference/applications/n8n.mdx",
215
215
  import.meta.url,
216
216
  ),
217
217
  ),
@@ -13,7 +13,8 @@
13
13
  "scripts": {
14
14
  "build": "tsc --build",
15
15
  "typecheck": "tsc --noEmit",
16
- "test": "vitest run"
16
+ "test": "vitest run",
17
+ "migrate:lastmile": "tsx scripts/migrate-lastmile.ts"
17
18
  },
18
19
  "dependencies": {
19
20
  "graphql": "^16.10.0"
@@ -0,0 +1,114 @@
1
+ # TEI LastMile → Twenty sync — Lambda deploy runbook
2
+
3
+ Deploy the nightly LastMile → Twenty delta sync as an AWS Lambda in **TEI's
4
+ account** (the same account as `n8n.lastmile-tei.com` and the LastMile RDS), and
5
+ invoke it nightly from n8n. The function reuses the exact code proven against
6
+ TEI's live Twenty; a full delta run is ~90s, so it fits Lambda's 15-minute
7
+ ceiling with wide headroom.
8
+
9
+ **Properties:** idempotent (upsert by `sourceId` + deletion mirror), reads
10
+ LastMile **read-only**, never provisions members, needs **no AWS credentials**
11
+ of its own (the ~4 CRM attachments Twenty can't ingest are reported, not fatal).
12
+
13
+ ---
14
+
15
+ ## 1. Build the deployment zip
16
+
17
+ From the repo root:
18
+
19
+ ```bash
20
+ pnpm install
21
+ bash plugins/twenty/scripts/build-lambda.sh
22
+ # → plugins/twenty/dist/tei-lastmile-sync-lambda.zip (handler: index.handler)
23
+ ```
24
+
25
+ The zip is self-contained (pg, bcryptjs, aws-sdk all inlined) — no `npm install`
26
+ in the account.
27
+
28
+ ## 2. Create the function
29
+
30
+ - **Runtime:** Node.js 22.x
31
+ - **Handler:** `index.handler`
32
+ - **Architecture:** arm64 or x86_64 (either)
33
+ - **Timeout:** 300s (run is ~90s; leaves headroom for a busier delta)
34
+ - **Memory:** 512 MB
35
+ - **Code:** upload `tei-lastmile-sync-lambda.zip`
36
+
37
+ ```bash
38
+ aws lambda create-function \
39
+ --function-name tei-lastmile-sync \
40
+ --runtime nodejs22.x --handler index.handler \
41
+ --timeout 300 --memory-size 512 \
42
+ --role arn:aws:iam::<ACCOUNT_ID>:role/<lambda-exec-role> \
43
+ --zip-file fileb://plugins/twenty/dist/tei-lastmile-sync-lambda.zip
44
+ # later updates: aws lambda update-function-code --function-name tei-lastmile-sync \
45
+ # --zip-file fileb://plugins/twenty/dist/tei-lastmile-sync-lambda.zip
46
+ ```
47
+
48
+ ## 3. Configuration (the three env vars)
49
+
50
+ | Variable | Value |
51
+ | ----------------------- | ---------------------------------------------------- |
52
+ | `TWENTY_PUBLIC_URL` | `https://crm.tei.thinkwork.ai` |
53
+ | `TWENTY_API_KEY` | Twenty workspace API key (Settings → API & Webhooks) |
54
+ | `LASTMILE_DATABASE_URL` | read-only DSN for the `dispatch` database |
55
+
56
+ Set them directly as Lambda environment variables, or (preferred) store the two
57
+ secrets in Secrets Manager and resolve them at start. If you use Secrets
58
+ Manager, add `secretsmanager:GetSecretValue` to the execution role.
59
+
60
+ ```bash
61
+ aws lambda update-function-configuration --function-name tei-lastmile-sync \
62
+ --environment "Variables={TWENTY_PUBLIC_URL=https://crm.tei.thinkwork.ai,TWENTY_API_KEY=***,LASTMILE_DATABASE_URL=***}"
63
+ ```
64
+
65
+ ## 4. Networking — the one thing that must be right
66
+
67
+ The function must reach **both** the LastMile RDS **and** `crm.tei.thinkwork.ai`.
68
+ Mirror wherever n8n already connects to the RDS:
69
+
70
+ - **In-VPC (cleanest):** attach the Lambda to the RDS's VPC/subnets, give it a
71
+ security group allowed to reach the RDS on 5432, and ensure the subnets have a
72
+ **NAT gateway** so it can still reach `crm.tei.thinkwork.ai` over HTTPS.
73
+ - **No-VPC (public):** the RDS is a public endpoint — the function reaches it and
74
+ Twenty over the internet, but the **RDS security group must admit the
75
+ function's egress IP** (the account's NAT/EIP). This is the same allowlisting
76
+ n8n already has.
77
+
78
+ Execution role needs at least `AWSLambdaBasicExecutionRole` (CloudWatch Logs),
79
+ plus `AWSLambdaVPCAccessExecutionRole` if you attach it to a VPC.
80
+
81
+ ## 5. Smoke test before scheduling
82
+
83
+ Always dry-run first (reads + reports, writes nothing):
84
+
85
+ ```bash
86
+ aws lambda invoke --function-name tei-lastmile-sync \
87
+ --payload '{"mode":"dry-run"}' --cli-binary-format raw-in-base64-out out.json
88
+ cat out.json # → {"ok":true,"failed":0,"report":{...parity...}}
89
+ ```
90
+
91
+ If `report.mode` is `dry-run` and the entity counts look right, you're wired
92
+ correctly. Then run once for real: `--payload '{"mode":"apply"}'` (or `'{}'` —
93
+ apply is the default).
94
+
95
+ ## 6. Wire n8n
96
+
97
+ 1. **Schedule Trigger** node — cron `0 3 * * *` (03:00 America/Chicago), or your
98
+ preferred window.
99
+ 2. **AWS Lambda** node → _Invoke_, region `us-east-1`, function
100
+ `tei-lastmile-sync`, invocation type `RequestResponse`. Leave the payload
101
+ empty (defaults to apply) or send `{"mode":"apply"}`.
102
+ 3. (Optional) an **IF** node on `{{$json.ok}}` / `{{$json.failed}}` to post a
103
+ Slack/email alert when a run reports soft failures.
104
+
105
+ n8n's AWS credential needs `lambda:InvokeFunction` on the function ARN.
106
+
107
+ ## 7. Operational invariants (human-owned)
108
+
109
+ - Keep Twenty's **"Create company when adding a new person"** workflow
110
+ **deactivated** — the API key can't toggle it, and it mislinks new people.
111
+ - **Rotate** the shared `TWENTY_REP_PASSWORD` once the validation window closes.
112
+ - A **cold re-seed** (empty Twenty) creates thousands of records at Twenty's rate
113
+ limit and can exceed 15 min — run that from the CLI/bundle, not this Lambda.
114
+ Steady-state nightly deltas are the Lambda's job.
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env bash
2
+ # Build a self-contained AWS Lambda deployment zip for the LastMile → Twenty
3
+ # nightly sync. The bundle inlines every dependency (pg, bcryptjs, aws-sdk),
4
+ # so the target account needs no `npm install` — just upload the zip.
5
+ #
6
+ # Usage: bash scripts/build-lambda.sh
7
+ # Output: plugins/twenty/dist/tei-lastmile-sync-lambda.zip (handler: index.handler)
8
+ set -euo pipefail
9
+
10
+ cd "$(dirname "$0")/.." # -> plugins/twenty
11
+ OUT_DIR="dist/tei-lastmile-sync-lambda"
12
+ ZIP="dist/tei-lastmile-sync-lambda.zip"
13
+
14
+ rm -rf "$OUT_DIR" "$ZIP"
15
+ mkdir -p "$OUT_DIR"
16
+
17
+ # CJS + .js so Lambda's Node runtime resolves `index.handler` with no wrapper.
18
+ npx esbuild scripts/lambda-handler.ts \
19
+ --bundle --platform=node --format=cjs --target=node20 \
20
+ --outfile="$OUT_DIR/index.js"
21
+
22
+ # The repo's plugins/twenty/package.json is "type":"module", which would make
23
+ # Node treat the CJS index.js as ESM. Ship a CommonJS marker inside the zip so
24
+ # the handler loads correctly regardless of the deploy environment.
25
+ printf '{\n "type": "commonjs"\n}\n' >"$OUT_DIR/package.json"
26
+
27
+ (cd "$OUT_DIR" && zip -q -r "../../$ZIP" index.js package.json)
28
+ echo "built $(cd "$(dirname "$ZIP")" && pwd)/$(basename "$ZIP")"
29
+ echo " handler: index.handler size: $(du -h "$ZIP" | cut -f1)"
@@ -0,0 +1,195 @@
1
+ #!/usr/bin/env npx tsx
2
+ /**
3
+ * One-off: make the Opportunity's Products table show Product, Quantity, and
4
+ * Amount.
5
+ *
6
+ * Twenty gives a new custom object a default TABLE view whose columns are
7
+ * `name` plus the audit fields (createdAt, createdBy, updatedAt, updatedBy).
8
+ * So the Products section rendered a row per line with no product, no quantity,
9
+ * and no amount — the data was all there, just never surfaced.
10
+ *
11
+ * This sets the opportunityProduct table view to:
12
+ * Product | Quantity | Amount | Line
13
+ * and hides the audit columns. It also orders the record-page field widget the
14
+ * same way.
15
+ *
16
+ * Idempotent: re-running reconciles the view to the same state.
17
+ *
18
+ * Usage:
19
+ * npx tsx scripts/configure-product-views.ts # dry-run
20
+ * npx tsx scripts/configure-product-views.ts --apply
21
+ */
22
+
23
+ import process from "node:process";
24
+
25
+ import { fetchObjectMetadata } from "./lib/schema-ensure";
26
+ import { TwentyClient, normalizeBaseUrl } from "./lib/twenty-client";
27
+
28
+ /**
29
+ * Exactly what Eric asked for: "Each line needs to be Product Name, Quantity and
30
+ * Amount." `name` is the record's label identifier (it holds the product name,
31
+ * or "Line 2" for the 173 lines whose brand did not map), so it leads. Twenty
32
+ * will not let the label identifier be hidden, which is also why it stays.
33
+ */
34
+ const VISIBLE_COLUMNS = ["name", "quantity", "amount"] as const;
35
+
36
+ /** Noise on a line item: audit trail, internals, and the product relation whose
37
+ * value already reads out through `name`. */
38
+ const HIDDEN_COLUMNS = [
39
+ "createdAt",
40
+ "createdBy",
41
+ "updatedAt",
42
+ "updatedBy",
43
+ "sourceId",
44
+ "sourceHash",
45
+ "isMobil",
46
+ "opportunity",
47
+ "lineNumber",
48
+ "product",
49
+ ] as const;
50
+
51
+ interface ViewField {
52
+ id: string;
53
+ fieldMetadataId: string;
54
+ isVisible: boolean;
55
+ position: number;
56
+ }
57
+
58
+ function requireEnv(name: string): string {
59
+ const value = process.env[name];
60
+ if (!value) throw new Error(`Missing required environment variable: ${name}`);
61
+ return value;
62
+ }
63
+
64
+ function log(message: string): void {
65
+ process.stderr.write(`${message}\n`);
66
+ }
67
+
68
+ async function main(): Promise<void> {
69
+ const apply = process.argv.slice(2).includes("--apply");
70
+ const client = new TwentyClient({
71
+ baseUrl: normalizeBaseUrl(requireEnv("TWENTY_PUBLIC_URL")),
72
+ authToken: requireEnv("TWENTY_API_KEY"),
73
+ });
74
+
75
+ const objects = await fetchObjectMetadata(client);
76
+ const line = objects.get("opportunityProduct");
77
+ if (!line) throw new Error('Twenty object "opportunityProduct" not found.');
78
+ const fieldIdByName = new Map(
79
+ [...line.fields.values()].map((field) => [field.name, field.id]),
80
+ );
81
+
82
+ const views = await client.requestWithRetry<{
83
+ getViews: Array<{ id: string; name: string; type: string }>;
84
+ }>(
85
+ "/metadata",
86
+ `query ConfigViews($objectMetadataId: String!) {
87
+ getViews(objectMetadataId: $objectMetadataId) { id name type }
88
+ }`,
89
+ { objectMetadataId: line.id },
90
+ );
91
+
92
+ const report: Record<string, unknown> = {
93
+ mode: apply ? "apply" : "dry-run",
94
+ views: views.getViews.map((view) => `${view.type}:${view.name}`),
95
+ };
96
+ const changes: string[] = [];
97
+
98
+ for (const view of views.getViews) {
99
+ const fields = await client.requestWithRetry<{
100
+ getViewFields: ViewField[];
101
+ }>(
102
+ "/metadata",
103
+ `query ConfigViewFields($viewId: String!) {
104
+ getViewFields(viewId: $viewId) { id fieldMetadataId isVisible position }
105
+ }`,
106
+ { viewId: view.id },
107
+ );
108
+ const existingByFieldId = new Map(
109
+ fields.getViewFields.map((field) => [field.fieldMetadataId, field]),
110
+ );
111
+
112
+ // Wanted columns, in order.
113
+ for (const [index, name] of VISIBLE_COLUMNS.entries()) {
114
+ const fieldMetadataId = fieldIdByName.get(name);
115
+ if (!fieldMetadataId) {
116
+ throw new Error(
117
+ `opportunityProduct.${name} not found; run restructure-products first.`,
118
+ );
119
+ }
120
+ const existing = existingByFieldId.get(fieldMetadataId);
121
+ if (existing?.isVisible && existing.position === index) continue;
122
+
123
+ changes.push(`${view.type}: show ${name} at ${index}`);
124
+ if (!apply) continue;
125
+
126
+ if (existing) {
127
+ await client.requestOnce(
128
+ "/metadata",
129
+ `mutation ConfigUpdateViewField($input: UpdateViewFieldInput!) {
130
+ updateViewField(input: $input) { id }
131
+ }`,
132
+ {
133
+ input: {
134
+ id: existing.id,
135
+ update: { isVisible: true, position: index },
136
+ },
137
+ },
138
+ );
139
+ } else {
140
+ await client.requestOnce(
141
+ "/metadata",
142
+ `mutation ConfigCreateViewField($input: CreateViewFieldInput!) {
143
+ createViewField(input: $input) { id }
144
+ }`,
145
+ {
146
+ input: {
147
+ viewId: view.id,
148
+ fieldMetadataId,
149
+ isVisible: true,
150
+ position: index,
151
+ size: 150,
152
+ },
153
+ },
154
+ );
155
+ }
156
+ }
157
+
158
+ // Everything else on the line is noise in a table of deal lines.
159
+ for (const name of HIDDEN_COLUMNS) {
160
+ const fieldMetadataId = fieldIdByName.get(name);
161
+ if (!fieldMetadataId) continue;
162
+ const existing = existingByFieldId.get(fieldMetadataId);
163
+ if (!existing || !existing.isVisible) continue;
164
+ changes.push(`${view.type}: hide ${name}`);
165
+ if (!apply) continue;
166
+ await client.requestOnce(
167
+ "/metadata",
168
+ `mutation ConfigHideViewField($input: UpdateViewFieldInput!) {
169
+ updateViewField(input: $input) { id }
170
+ }`,
171
+ { input: { id: existing.id, update: { isVisible: false } } },
172
+ );
173
+ }
174
+ }
175
+
176
+ // The opportunity's `products` relation is left as-is (visible chips). An
177
+ // earlier version hid it and/or replaced it with a denormalized text field;
178
+ // both were wrong. The line records (Product / Quantity / Amount) are the
179
+ // correct structured model. A real inline COLUMNAR table on the record page
180
+ // is not something stock Twenty renders for a one-to-many relation — that
181
+ // needs a custom Twenty-app widget, scoped separately.
182
+
183
+ report.changes =
184
+ changes.length > 0 ? changes : "none — views already correct";
185
+ if (apply && changes.length > 0)
186
+ log(`applied ${changes.length} view changes`);
187
+ process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
188
+ }
189
+
190
+ main().catch((error) => {
191
+ process.stderr.write(
192
+ `${error instanceof Error ? error.message : String(error)}\n`,
193
+ );
194
+ process.exit(1);
195
+ });