@graph8/sdk 0.373.0 → 0.374.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 (279) hide show
  1. package/README.md +3 -3
  2. package/blocks/_foundation/README.md +47 -0
  3. package/blocks/_foundation/block.json +13 -0
  4. package/blocks/_foundation/components/auth-gate.tsx +363 -0
  5. package/blocks/_foundation/server/auth.ts +815 -0
  6. package/blocks/campaigns/README.md +1 -1
  7. package/blocks/campaigns/block.json +34 -1
  8. package/blocks/campaigns/components/campaign-links.tsx +180 -0
  9. package/blocks/company-intelligence/components/research-run.tsx +6 -2
  10. package/blocks/crm/README.md +19 -0
  11. package/blocks/crm/block.json +205 -6
  12. package/blocks/crm/components/bulk-update.tsx +119 -0
  13. package/blocks/crm/components/contact-documents-tab.tsx +144 -0
  14. package/blocks/crm/components/contact-insights-tab.tsx +118 -0
  15. package/blocks/crm/components/contact-lookup-link.tsx +47 -6
  16. package/blocks/crm/components/contact-row-actions.tsx +43 -0
  17. package/blocks/crm/components/contact-tag-chips.tsx +113 -0
  18. package/blocks/crm/components/contacts-screen.tsx +46 -5
  19. package/blocks/crm/components/owner-assign.tsx +132 -0
  20. package/blocks/crm/components/record-open-links.tsx +79 -0
  21. package/blocks/crm/components/tag-assign.tsx +112 -0
  22. package/blocks/crm/components/tag-manager.tsx +178 -0
  23. package/blocks/crm/components/unlock-info-button.tsx +82 -0
  24. package/blocks/crm/components/verify-email-button.tsx +93 -0
  25. package/blocks/crm/hooks/use-contact-tags.ts +29 -0
  26. package/blocks/crm/lib/record-actions.ts +102 -0
  27. package/blocks/deals/README.md +1 -0
  28. package/blocks/deals/block.json +17 -0
  29. package/blocks/deals/components/create-deal-button.tsx +109 -0
  30. package/blocks/deals/components/deal-detail.tsx +5 -3
  31. package/blocks/dialer/components/call-log-screen.tsx +6 -1
  32. package/blocks/documents/README.md +86 -0
  33. package/blocks/documents/block.json +378 -0
  34. package/blocks/documents/components/account-bar.tsx +48 -0
  35. package/blocks/documents/components/correspondence-form.tsx +95 -0
  36. package/blocks/documents/components/correspondence-import.tsx +106 -0
  37. package/blocks/documents/components/correspondence-panel.tsx +191 -0
  38. package/blocks/documents/components/documents-screen.tsx +76 -0
  39. package/blocks/documents/components/library-document-detail.tsx +175 -0
  40. package/blocks/documents/components/library-document-form.tsx +105 -0
  41. package/blocks/documents/components/library-folder-form.tsx +74 -0
  42. package/blocks/documents/components/library-panel.tsx +235 -0
  43. package/blocks/documents/components/library-upload.tsx +160 -0
  44. package/blocks/documents/components/question-row.tsx +130 -0
  45. package/blocks/documents/components/questionnaire-detail.tsx +197 -0
  46. package/blocks/documents/components/questionnaires-panel.tsx +173 -0
  47. package/blocks/documents/components/record-documents-tab.tsx +109 -0
  48. package/blocks/documents/lib/documents-types.ts +252 -0
  49. package/blocks/documents/lib/download.ts +64 -0
  50. package/blocks/documents/recipes/follow-a-questionnaire.ts +26 -0
  51. package/blocks/documents/recipes/log-account-correspondence.ts +27 -0
  52. package/blocks/documents/recipes/organize-the-library.ts +25 -0
  53. package/blocks/email-signatures/README.md +123 -0
  54. package/blocks/email-signatures/block.json +341 -0
  55. package/blocks/email-signatures/components/email-signatures-screen.tsx +166 -0
  56. package/blocks/email-signatures/components/signature-assign-template.tsx +119 -0
  57. package/blocks/email-signatures/components/signature-block-editor.tsx +87 -0
  58. package/blocks/email-signatures/components/signature-campaign-form.tsx +165 -0
  59. package/blocks/email-signatures/components/signature-campaigns.tsx +75 -0
  60. package/blocks/email-signatures/components/signature-identity-form.tsx +131 -0
  61. package/blocks/email-signatures/components/signature-mailboxes.tsx +195 -0
  62. package/blocks/email-signatures/components/signature-policy.tsx +130 -0
  63. package/blocks/email-signatures/components/signature-preview.tsx +119 -0
  64. package/blocks/email-signatures/components/signature-report.tsx +77 -0
  65. package/blocks/email-signatures/components/signature-template-details.tsx +112 -0
  66. package/blocks/email-signatures/components/signature-template-form.tsx +109 -0
  67. package/blocks/email-signatures/hooks/use-signatures-data.ts +93 -0
  68. package/blocks/email-signatures/lib/signatures-types.ts +392 -0
  69. package/blocks/email-signatures/recipes/assign-a-signature-to-a-new-hire.ts +32 -0
  70. package/blocks/email-signatures/recipes/create-a-template-and-make-it-default.ts +35 -0
  71. package/blocks/email-signatures/recipes/signature-engagement-report.ts +16 -0
  72. package/blocks/enrichment/README.md +22 -4
  73. package/blocks/enrichment/block.json +145 -5
  74. package/blocks/enrichment/components/auto-enrichment-configs.tsx +134 -0
  75. package/blocks/enrichment/components/auto-enrichment-settings.tsx +82 -0
  76. package/blocks/enrichment/components/bulk-enrich-panel.tsx +7 -7
  77. package/blocks/enrichment/components/enrichment-activity.tsx +93 -0
  78. package/blocks/enrichment/components/enrichment-job-detail.tsx +18 -0
  79. package/blocks/enrichment/components/enrichment-screen.tsx +34 -3
  80. package/blocks/enrichment/components/list-pipelines.tsx +97 -0
  81. package/blocks/enrichment/components/pipeline-detail.tsx +214 -0
  82. package/blocks/enrichment/components/pipeline-editor.tsx +144 -0
  83. package/blocks/enrichment/components/pipeline-templates.tsx +90 -0
  84. package/blocks/enrichment/lib/enrichment-types.ts +12 -0
  85. package/blocks/enrichment/lib/pipeline-types.ts +234 -0
  86. package/blocks/forms/README.md +2 -0
  87. package/blocks/forms/components/forms-screen.tsx +6 -3
  88. package/blocks/inbox/README.md +40 -4
  89. package/blocks/inbox/block.json +130 -8
  90. package/blocks/inbox/components/blocked-senders.tsx +103 -0
  91. package/blocks/inbox/components/compose-message.tsx +193 -0
  92. package/blocks/inbox/components/inbox-screen.tsx +54 -6
  93. package/blocks/inbox/components/inbox-search.tsx +106 -0
  94. package/blocks/inbox/components/tag-manager.tsx +136 -0
  95. package/blocks/inbox/components/thread-actions.tsx +133 -0
  96. package/blocks/inbox/components/thread-view.tsx +2 -0
  97. package/blocks/inbox/hooks/use-inbox-mailboxes.ts +31 -0
  98. package/blocks/inbox/lib/inbox-manage.ts +227 -0
  99. package/blocks/newsletter/components/newsletter-screen.tsx +3 -1
  100. package/blocks/pages/block.json +10 -1
  101. package/blocks/pages/hooks/use-landing-page-paths.ts +60 -0
  102. package/blocks/programmatic-pages/README.md +82 -0
  103. package/blocks/programmatic-pages/block.json +292 -0
  104. package/blocks/programmatic-pages/components/programmatic-campaign-form.tsx +136 -0
  105. package/blocks/programmatic-pages/components/programmatic-generate.tsx +112 -0
  106. package/blocks/programmatic-pages/components/programmatic-generated-pages.tsx +210 -0
  107. package/blocks/programmatic-pages/components/programmatic-overview.tsx +84 -0
  108. package/blocks/programmatic-pages/components/programmatic-pages-screen.tsx +221 -0
  109. package/blocks/programmatic-pages/components/programmatic-preview.tsx +66 -0
  110. package/blocks/programmatic-pages/components/programmatic-publish.tsx +102 -0
  111. package/blocks/programmatic-pages/components/programmatic-settings.tsx +96 -0
  112. package/blocks/programmatic-pages/components/programmatic-source-fields.tsx +75 -0
  113. package/blocks/programmatic-pages/components/programmatic-source.tsx +106 -0
  114. package/blocks/programmatic-pages/components/programmatic-variables.tsx +84 -0
  115. package/blocks/programmatic-pages/hooks/use-programmatic-workspace.ts +19 -0
  116. package/blocks/programmatic-pages/lib/programmatic-types.ts +330 -0
  117. package/blocks/programmatic-pages/recipes/create-a-programmatic-campaign.ts +23 -0
  118. package/blocks/programmatic-pages/recipes/render-and-follow.ts +21 -0
  119. package/blocks/programmatic-pages/recipes/review-then-publish.ts +17 -0
  120. package/blocks/quotes/README.md +25 -1
  121. package/blocks/quotes/block.json +123 -6
  122. package/blocks/quotes/components/manual-products.tsx +144 -0
  123. package/blocks/quotes/components/quote-batch-actions.tsx +86 -0
  124. package/blocks/quotes/components/quote-documents.tsx +94 -0
  125. package/blocks/quotes/components/quote-evidence.tsx +95 -0
  126. package/blocks/quotes/components/quote-list.tsx +23 -5
  127. package/blocks/quotes/components/quote-settings.tsx +184 -0
  128. package/blocks/quotes/components/quote-templates.tsx +189 -0
  129. package/blocks/quotes/components/quote-viewer.tsx +16 -3
  130. package/blocks/quotes/components/quotes-screen.tsx +15 -5
  131. package/blocks/quotes/lib/quote-admin.ts +264 -0
  132. package/blocks/sequencer/README.md +55 -0
  133. package/blocks/sequencer/block.json +172 -5
  134. package/blocks/sequencer/components/enrollment-withdraw.tsx +71 -0
  135. package/blocks/sequencer/components/sequence-builder.tsx +13 -3
  136. package/blocks/sequencer/components/sequence-campaign-link.tsx +78 -0
  137. package/blocks/sequencer/components/sequence-detail.tsx +6 -0
  138. package/blocks/sequencer/components/sequence-edit-form.tsx +111 -0
  139. package/blocks/sequencer/components/sequence-enrollments.tsx +2 -1
  140. package/blocks/sequencer/components/sequence-manage-actions.tsx +119 -0
  141. package/blocks/sequencer/components/sequence-senders-panel.tsx +84 -0
  142. package/blocks/sequencer/components/sequence-templates.tsx +160 -0
  143. package/blocks/sequencer/components/sequences-screen.tsx +36 -6
  144. package/blocks/sequencer/components/step-ai-assist.tsx +196 -0
  145. package/blocks/sequencer/components/step-editor.tsx +10 -1
  146. package/blocks/sequencer/components/step-templates.tsx +224 -0
  147. package/blocks/sequencer/lib/sequencer-manage.ts +235 -0
  148. package/blocks/sequencer/lib/sequencer-utils.ts +4 -0
  149. package/blocks/signals/README.md +39 -0
  150. package/blocks/signals/block.json +287 -0
  151. package/blocks/signals/components/auto-seed-panel.tsx +111 -0
  152. package/blocks/signals/components/listener-form.tsx +184 -0
  153. package/blocks/signals/components/matched-posts.tsx +195 -0
  154. package/blocks/signals/components/post-engagements.tsx +114 -0
  155. package/blocks/signals/components/record-social-tab.tsx +49 -0
  156. package/blocks/signals/components/social-listeners-screen.tsx +250 -0
  157. package/blocks/signals/components/social-settings.tsx +128 -0
  158. package/blocks/signals/components/source-lists.tsx +83 -0
  159. package/blocks/signals/components/top-engagers.tsx +68 -0
  160. package/blocks/signals/components/x-integration.tsx +129 -0
  161. package/blocks/signals/lib/signals-types.ts +228 -0
  162. package/blocks/signals/recipes/preview-and-create-a-listener.ts +16 -0
  163. package/blocks/signals/recipes/spend-against-caps.ts +14 -0
  164. package/blocks/signals/recipes/triage-matched-posts.ts +18 -0
  165. package/blocks/visitors/components/traffic-chart.tsx +3 -2
  166. package/blocks/web-analytics/README.md +102 -0
  167. package/blocks/web-analytics/block.json +292 -0
  168. package/blocks/web-analytics/components/acquisition-tab.tsx +154 -0
  169. package/blocks/web-analytics/components/analytics-filters.tsx +161 -0
  170. package/blocks/web-analytics/components/audience-tab.tsx +135 -0
  171. package/blocks/web-analytics/components/conversions-tab.tsx +128 -0
  172. package/blocks/web-analytics/components/engagement-tab.tsx +123 -0
  173. package/blocks/web-analytics/components/overview-tab.tsx +102 -0
  174. package/blocks/web-analytics/components/pages-tab.tsx +148 -0
  175. package/blocks/web-analytics/components/ranked-table.tsx +103 -0
  176. package/blocks/web-analytics/components/realtime-tab.tsx +103 -0
  177. package/blocks/web-analytics/components/report-card.tsx +71 -0
  178. package/blocks/web-analytics/components/web-analytics-screen.tsx +90 -0
  179. package/blocks/web-analytics/hooks/use-report.ts +48 -0
  180. package/blocks/web-analytics/lib/analytics-scope.ts +155 -0
  181. package/blocks/web-analytics/lib/page-path.ts +14 -0
  182. package/blocks/web-analytics/lib/web-analytics-types.ts +187 -0
  183. package/blocks/web-analytics/recipes/best-landing-pages.ts +26 -0
  184. package/blocks/web-analytics/recipes/form-conversion-check.ts +19 -0
  185. package/blocks/web-analytics/recipes/weekly-traffic-report.ts +37 -0
  186. package/blocks/workbench/README.md +54 -2
  187. package/blocks/workbench/block.json +234 -5
  188. package/blocks/workbench/components/workbench-auto-enrich.tsx +88 -0
  189. package/blocks/workbench/components/workbench-automation.tsx +36 -0
  190. package/blocks/workbench/components/workbench-connections.tsx +136 -0
  191. package/blocks/workbench/components/workbench-crm-import.tsx +91 -0
  192. package/blocks/workbench/components/workbench-datafile-import.tsx +154 -0
  193. package/blocks/workbench/components/workbench-grid.tsx +70 -7
  194. package/blocks/workbench/components/workbench-pipelines.tsx +173 -0
  195. package/blocks/workbench/components/workbench-row-actions.tsx +8 -1
  196. package/blocks/workbench/components/workbench-schedule.tsx +191 -0
  197. package/blocks/workbench/components/workbench-screen.tsx +50 -17
  198. package/blocks/workbench/components/workbench-send-to-sequence.tsx +116 -0
  199. package/blocks/workbench/components/workbench-table-settings.tsx +53 -0
  200. package/blocks/workbench/components/workbench-triggers.tsx +178 -0
  201. package/blocks/workbench/hooks/use-connected-crms.ts +29 -0
  202. package/blocks/workbench/lib/api-status.ts +8 -0
  203. package/blocks/workbench/lib/automation-types.ts +257 -0
  204. package/dist/cli.js +257 -51
  205. package/dist/index.browser.js +1 -1
  206. package/dist/index.browser.js.map +1 -1
  207. package/dist/index.browser.mjs +1 -1
  208. package/dist/index.browser.mjs.map +1 -1
  209. package/dist/index.js +1 -1
  210. package/dist/index.js.map +1 -1
  211. package/dist/index.mjs +1 -1
  212. package/dist/index.mjs.map +1 -1
  213. package/dist/react.browser.js +1 -1
  214. package/dist/react.browser.js.map +1 -1
  215. package/dist/react.browser.mjs +1 -1
  216. package/dist/react.browser.mjs.map +1 -1
  217. package/dist/react.js +1 -1
  218. package/dist/react.js.map +1 -1
  219. package/dist/react.mjs +1 -1
  220. package/dist/react.mjs.map +1 -1
  221. package/dist/types/blocks/auth.d.ts +45 -0
  222. package/dist/types/blocks/create.d.ts +5 -2
  223. package/dist/types/version.d.ts +2 -2
  224. package/docs/api/forms.md +1 -1
  225. package/package.json +1 -1
  226. package/stacks/6sense/pages/contacts.tsx.tmpl +2 -0
  227. package/stacks/6sense.json +20 -0
  228. package/stacks/apollo/pages/calls.tsx.tmpl +9 -9
  229. package/stacks/apollo/pages/campaigns.tsx.tmpl +27 -0
  230. package/stacks/apollo/pages/contacts.tsx.tmpl +6 -0
  231. package/stacks/apollo/pages/inbox-contact.tsx.tmpl +3 -3
  232. package/stacks/apollo/pages/lists.tsx.tmpl +14 -2
  233. package/stacks/apollo/pages/sequences.tsx.tmpl +8 -2
  234. package/stacks/apollo.json +33 -2
  235. package/stacks/clay/pages/contacts.tsx.tmpl +2 -0
  236. package/stacks/clay/pages/lists.tsx.tmpl +14 -2
  237. package/stacks/gong/pages/calls.tsx.tmpl +9 -9
  238. package/stacks/gong/pages/contacts.tsx.tmpl +2 -0
  239. package/stacks/hootsuite/pages/contacts.tsx.tmpl +2 -0
  240. package/stacks/hootsuite.json +10 -0
  241. package/stacks/hubspot/pages/campaigns.tsx.tmpl +30 -0
  242. package/stacks/hubspot/pages/companies.tsx.tmpl +2 -0
  243. package/stacks/hubspot/pages/contacts.tsx.tmpl +6 -0
  244. package/stacks/hubspot/pages/deals.tsx.tmpl +4 -0
  245. package/stacks/hubspot/pages/forms.tsx.tmpl +36 -0
  246. package/stacks/hubspot/pages/inbox.tsx.tmpl +2 -2
  247. package/stacks/hubspot/pages/sequences.tsx.tmpl +8 -2
  248. package/stacks/hubspot/pages/web-analytics.tsx.tmpl +14 -0
  249. package/stacks/hubspot.json +45 -2
  250. package/stacks/intercom/pages/inbox-contact.tsx.tmpl +3 -3
  251. package/stacks/intercom.json +10 -0
  252. package/stacks/mailchimp/pages/contacts.tsx.tmpl +2 -0
  253. package/stacks/mailchimp/pages/forms.tsx.tmpl +36 -0
  254. package/stacks/mailchimp.json +2 -1
  255. package/stacks/outreach/pages/calls.tsx.tmpl +9 -9
  256. package/stacks/outreach/pages/campaigns.tsx.tmpl +27 -0
  257. package/stacks/outreach/pages/contacts.tsx.tmpl +2 -0
  258. package/stacks/outreach/pages/inbox-contact.tsx.tmpl +3 -3
  259. package/stacks/outreach/pages/sequences.tsx.tmpl +8 -2
  260. package/stacks/outreach.json +13 -2
  261. package/stacks/pipedrive/pages/contacts.tsx.tmpl +2 -0
  262. package/stacks/pipedrive/pages/inbox.tsx.tmpl +2 -2
  263. package/stacks/pipedrive.json +10 -0
  264. package/stacks/salesforce/pages/companies.tsx.tmpl +2 -0
  265. package/stacks/salesforce/pages/contacts.tsx.tmpl +6 -0
  266. package/stacks/salesforce/pages/deals.tsx.tmpl +4 -0
  267. package/stacks/salesforce.json +10 -0
  268. package/stacks/zoominfo/pages/contacts.tsx.tmpl +4 -0
  269. package/stacks/zoominfo/pages/lists.tsx.tmpl +14 -2
  270. package/stacks/zoominfo.json +20 -0
  271. package/styles/styles.css +1 -1
  272. package/templates/auth/auth-route.ts.tmpl +16 -0
  273. package/templates/auth/g8.auth.anonymous.ts.tmpl +15 -0
  274. package/templates/auth/g8.auth.basic.ts.tmpl +17 -0
  275. package/templates/auth/g8.auth.jwt.ts.tmpl +23 -0
  276. package/templates/auth/g8.auth.password.ts.tmpl +18 -0
  277. package/templates/auth/g8.auth.session.ts.tmpl +23 -0
  278. package/templates/auth/middleware.ts.tmpl +14 -0
  279. package/templates/next-app/README.md.tmpl +5 -0
@@ -0,0 +1,264 @@
1
+ "use client";
2
+
3
+ /**
4
+ * Pure helpers for the quote administration screens: templates, org settings, the document library, evidence
5
+ * uploads, batch actions and manual products. No network, so the rules below are unit-tested directly.
6
+ *
7
+ * What the API really does (read from the handlers, not assumed):
8
+ * - PUT /quote-templates/{id} REPLACES the template. A field left out is cleared, so the form always sends the whole
9
+ * template. The API body has no `accepted_payment_methods`, `marketing_rights` or `contract_start_on_signature`,
10
+ * so saving through the API resets those three (see `fieldsLostOnTemplateSave`).
11
+ * - PUT /quote-settings only writes non-null values: a field that has a value cannot be cleared through the API
12
+ * (see `unclearableSettings`). Everything it does accept is a partial update, so we send only what changed.
13
+ * - Uploads are PDF only. The API allows 10 MB, but the app proxy caps a whole request at 5 MiB (413 beyond that),
14
+ * so the screens refuse anything over `UPLOAD_LIMIT_BYTES` up front.
15
+ * - POST /quotes/batch takes `quote_ids` (1 to 200) and an action of archive, expire or revoke, and answers a row per
16
+ * id (`success`, `skipped` or `error`). A 200 means the batch ran, not that every quote moved. Expire skips a draft or
17
+ * a quote with a final outcome; revoke skips anything not sent, viewed or link generated; archive skips an archived quote.
18
+ */
19
+ import { asRec, text, type LineItem, type Rec } from "./quotes";
20
+
21
+ /** The app proxy rejects a request body over 5 MiB (blocks/_foundation/server/proxy.ts DEFAULT_MAX_BODY). */
22
+ export const UPLOAD_LIMIT_BYTES = 4_800_000;
23
+ export const UPLOAD_LIMIT_LABEL = "about 5 MB";
24
+ export const BATCH_MAX = 200;
25
+
26
+ /** Why a file cannot be uploaded as a quote PDF, or null when it can. */
27
+ export function pdfProblem(file: File | null | undefined): string | null {
28
+ if (!file) return "Choose a PDF file first.";
29
+ if (!file.name.toLowerCase().endsWith(".pdf")) return "Only PDF files are accepted.";
30
+ if (file.size === 0) return "That file is empty.";
31
+ if (file.size > UPLOAD_LIMIT_BYTES) {
32
+ return `That file is ${(file.size / 1_000_000).toFixed(1)} MB. This app accepts PDFs up to ${UPLOAD_LIMIT_LABEL} (the whole request is capped); compress the PDF and try again.`;
33
+ }
34
+ return null;
35
+ }
36
+
37
+ export function fileSize(bytes: unknown): string {
38
+ const n = Number(bytes);
39
+ if (!Number.isFinite(n) || n <= 0) return "";
40
+ return n >= 1_000_000 ? `${(n / 1_000_000).toFixed(1)} MB` : `${Math.max(1, Math.round(n / 1000))} KB`;
41
+ }
42
+
43
+ // ---------------------------------------------------------------------------
44
+ // Templates
45
+ // ---------------------------------------------------------------------------
46
+
47
+ export const DURATION_UNITS = ["months", "weeks", "days"] as const;
48
+ export type DurationUnit = (typeof DURATION_UNITS)[number];
49
+
50
+ export interface TemplateDraft {
51
+ name: string;
52
+ description: string;
53
+ default_payment_terms: string;
54
+ default_valid_days: string;
55
+ terms_override: string;
56
+ is_default: boolean;
57
+ contract_duration_value: string;
58
+ contract_duration_unit: DurationUnit | "";
59
+ document_ids: string[];
60
+ lines: LineItem[];
61
+ }
62
+
63
+ export const emptyTemplateDraft = (): TemplateDraft => ({
64
+ name: "", description: "", default_payment_terms: "", default_valid_days: "", terms_override: "", is_default: false,
65
+ contract_duration_value: "", contract_duration_unit: "", document_ids: [], lines: [],
66
+ });
67
+
68
+ const numText = (v: unknown): string => (v == null || v === "" ? "" : String(Number(v)));
69
+
70
+ /** A stored template row -> the form. A template that only has the older `contract_months` shows it as months. */
71
+ export function draftFromTemplate(row: Rec, lines: LineItem[]): TemplateDraft {
72
+ const hasDuration = row.contract_duration_value != null;
73
+ const unit = text(row.contract_duration_unit);
74
+ return {
75
+ name: text(row.name),
76
+ description: text(row.description),
77
+ default_payment_terms: text(row.default_payment_terms),
78
+ default_valid_days: numText(row.default_valid_days),
79
+ terms_override: text(row.terms_override),
80
+ is_default: row.is_default === true,
81
+ contract_duration_value: hasDuration ? numText(row.contract_duration_value) : numText(row.contract_months),
82
+ contract_duration_unit: hasDuration ? ((DURATION_UNITS as readonly string[]).includes(unit) ? (unit as DurationUnit) : "months") : row.contract_months != null ? "months" : "",
83
+ document_ids: Array.isArray(row.document_ids) ? row.document_ids.map(String) : [],
84
+ lines,
85
+ };
86
+ }
87
+
88
+ /** What is wrong with the draft, in the order the form shows it. Empty means it can be saved. */
89
+ export function templateProblems(d: TemplateDraft): string[] {
90
+ const out: string[] = [];
91
+ if (!d.name.trim()) out.push("Give the template a name.");
92
+ if (d.name.trim().length > 255) out.push("The name can be at most 255 characters.");
93
+ if (d.default_valid_days) {
94
+ const n = Number(d.default_valid_days);
95
+ if (!Number.isInteger(n) || n < 1 || n > 365) out.push("Valid days must be a whole number from 1 to 365.");
96
+ }
97
+ const hasValue = d.contract_duration_value !== "";
98
+ if (hasValue !== (d.contract_duration_unit !== "")) out.push("Set both the contract length and its unit, or neither.");
99
+ if (hasValue) {
100
+ const n = Number(d.contract_duration_value);
101
+ const max = d.contract_duration_unit === "months" ? 120 : 3650;
102
+ if (!Number.isFinite(n) || n < 0.1 || n > max) out.push(`A contract length in ${d.contract_duration_unit || "months"} must be from 0.1 to ${max}.`);
103
+ }
104
+ return out;
105
+ }
106
+
107
+ /** The full PUT/POST body. Blank optional fields are sent as null so a save clears them, which is what PUT means. */
108
+ export function templateBody(d: TemplateDraft, lines: Rec[]): Rec {
109
+ const hasLength = d.contract_duration_value !== "" && d.contract_duration_unit !== "";
110
+ const length = hasLength ? Number(d.contract_duration_value) : null;
111
+ return {
112
+ name: d.name.trim(),
113
+ description: d.description.trim() || null,
114
+ default_payment_terms: d.default_payment_terms || null,
115
+ default_valid_days: d.default_valid_days ? Number(d.default_valid_days) : null,
116
+ terms_override: d.terms_override.trim() || null,
117
+ is_default: d.is_default,
118
+ contract_duration_value: length,
119
+ contract_duration_unit: hasLength ? d.contract_duration_unit : null,
120
+ contract_months: hasLength && d.contract_duration_unit === "months" ? length : null,
121
+ document_ids: d.document_ids,
122
+ default_line_items: lines,
123
+ };
124
+ }
125
+
126
+ /**
127
+ * Settings the app lets people set on a template that the API's PUT body cannot carry, and that therefore reset when the
128
+ * template is saved from here. Used to warn before saving, never to guess: it reads the stored row.
129
+ */
130
+ export function fieldsLostOnTemplateSave(row: Rec): string[] {
131
+ const out: string[] = [];
132
+ if (Array.isArray(row.accepted_payment_methods) && row.accepted_payment_methods.length > 0) out.push("accepted payment methods");
133
+ if (Array.isArray(row.marketing_rights) && row.marketing_rights.length > 0) out.push("marketing rights");
134
+ if (row.contract_start_on_signature === false) out.push("the contract starts on signature setting (it goes back to on)");
135
+ return out;
136
+ }
137
+
138
+ // ---------------------------------------------------------------------------
139
+ // Settings
140
+ // ---------------------------------------------------------------------------
141
+
142
+ export type SettingsField =
143
+ | "company_name" | "company_logo_url" | "company_address" | "company_phone" | "company_email" | "default_terms" | "default_privacy"
144
+ | "terms_url" | "privacy_url" | "footer_text" | "accent_color" | "quote_number_prefix" | "default_payment_terms" | "sender_name" | "sender_email"
145
+ | "sender_mode" | "default_billing_provider" | "quote_acceptance_roam_channel_id" | "quote_acceptance_roam_channel_name";
146
+ export type SettingsBool = "auto_close_deal_on_acceptance" | "auto_advance_deal_on_quote_send" | "quote_acceptance_roam_notifications_enabled";
147
+
148
+ export const TEXT_FIELDS: SettingsField[] = [
149
+ "company_name", "company_logo_url", "company_address", "company_phone", "company_email", "default_terms", "default_privacy", "terms_url", "privacy_url",
150
+ "footer_text", "accent_color", "quote_number_prefix", "default_payment_terms", "sender_name", "sender_email", "sender_mode", "default_billing_provider",
151
+ "quote_acceptance_roam_channel_id", "quote_acceptance_roam_channel_name",
152
+ ];
153
+ export const BOOL_FIELDS: SettingsBool[] = ["auto_close_deal_on_acceptance", "auto_advance_deal_on_quote_send", "quote_acceptance_roam_notifications_enabled"];
154
+ export const BILLING_FIELDS = ["legal_name", "primary_email", "billing_email", "address"] as const;
155
+ /** The billing providers that actually provision on acceptance. QuickBooks, Xero and Chargebee are registered stubs. */
156
+ export const BILLING_PROVIDERS: Array<[string, string]> = [["none", "Manual (finance bills it elsewhere)"], ["stripe", "Stripe (invoice or subscription on acceptance)"]];
157
+
158
+ export interface TermsEntry { name: string; payment_terms: string; content: string }
159
+
160
+ export interface SettingsDraft {
161
+ text: Record<SettingsField, string>;
162
+ flags: Record<SettingsBool, boolean>;
163
+ default_valid_days: string;
164
+ required_billing_fields: string[];
165
+ terms_library: TermsEntry[];
166
+ }
167
+
168
+ export function draftFromSettings(row: Rec): SettingsDraft {
169
+ const t = {} as Record<SettingsField, string>;
170
+ for (const k of TEXT_FIELDS) t[k] = text(row[k]);
171
+ const f = {} as Record<SettingsBool, boolean>;
172
+ for (const k of BOOL_FIELDS) f[k] = row[k] === true;
173
+ const lib = Array.isArray(row.terms_library) ? row.terms_library : [];
174
+ return {
175
+ text: t, flags: f,
176
+ default_valid_days: numText(row.default_valid_days),
177
+ required_billing_fields: Array.isArray(row.required_billing_fields) ? row.required_billing_fields.map(String) : [],
178
+ terms_library: lib.map((e) => ({ name: text(asRec(e).name), payment_terms: text(asRec(e).payment_terms), content: text(asRec(e).content) })),
179
+ };
180
+ }
181
+
182
+ const sameList = (a: unknown[], b: unknown[]) => a.length === b.length && a.every((x, i) => JSON.stringify(x) === JSON.stringify(b[i]));
183
+
184
+ /** Only the fields that differ from the loaded settings. A field that was blank and is still blank is not sent. */
185
+ export function settingsChanges(was: SettingsDraft, now: SettingsDraft): Rec {
186
+ const out: Rec = {};
187
+ for (const k of TEXT_FIELDS) if (now.text[k] !== was.text[k] && now.text[k].trim() !== "") out[k] = now.text[k].trim();
188
+ for (const k of BOOL_FIELDS) if (now.flags[k] !== was.flags[k]) out[k] = now.flags[k];
189
+ if (now.default_valid_days !== was.default_valid_days && now.default_valid_days !== "") out.default_valid_days = Number(now.default_valid_days);
190
+ if (!sameList(now.required_billing_fields, was.required_billing_fields)) out.required_billing_fields = now.required_billing_fields;
191
+ if (!sameList(now.terms_library, was.terms_library)) out.terms_library = now.terms_library.map((e) => ({ name: e.name.trim(), payment_terms: e.payment_terms, content: e.content }));
192
+ return out;
193
+ }
194
+
195
+ /** Fields that had a value and are now blank: the API ignores null, so the save would silently keep the old value. */
196
+ export function unclearableSettings(was: SettingsDraft, now: SettingsDraft): string[] {
197
+ const out: string[] = [];
198
+ for (const k of TEXT_FIELDS) if (was.text[k].trim() !== "" && now.text[k].trim() === "") out.push(k);
199
+ if (was.default_valid_days !== "" && now.default_valid_days === "") out.push("default_valid_days");
200
+ return out;
201
+ }
202
+
203
+ export function settingsProblems(now: SettingsDraft): string[] {
204
+ const out: string[] = [];
205
+ const days = Number(now.default_valid_days);
206
+ if (now.default_valid_days && (!Number.isInteger(days) || days < 1)) out.push("Default valid days must be a whole number of at least 1.");
207
+ if (now.text.sender_email && !/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(now.text.sender_email.trim())) out.push("The sender email is not a valid address.");
208
+ if (now.text.company_email && !/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(now.text.company_email.trim())) out.push("The company email is not a valid address.");
209
+ if (now.terms_library.some((e) => !e.name.trim())) out.push("Every saved terms entry needs a name.");
210
+ return out;
211
+ }
212
+
213
+ export const FIELD_LABEL: Record<string, string> = {
214
+ company_name: "Company name", company_logo_url: "Logo URL", company_address: "Company address", company_phone: "Company phone", company_email: "Company email",
215
+ default_terms: "Default terms", default_privacy: "Default privacy text", terms_url: "Terms URL", privacy_url: "Privacy URL", footer_text: "Footer text", accent_color: "Accent color",
216
+ quote_number_prefix: "Quote number prefix", default_payment_terms: "Default payment terms", sender_name: "Sender name", sender_email: "Sender email", sender_mode: "Who quotes are sent as",
217
+ default_billing_provider: "Billing provider", default_valid_days: "Default valid days", quote_acceptance_roam_channel_id: "Roam channel id", quote_acceptance_roam_channel_name: "Roam channel name",
218
+ };
219
+
220
+ /** What a settings save does beyond storing text, so the confirm can say it. Empty means a plain save needs no confirm. */
221
+ export function settingsEffects(changes: Rec, was: SettingsDraft): string[] {
222
+ const out: string[] = [];
223
+ if ("sender_email" in changes || "sender_name" in changes || "sender_mode" in changes) out.push("Every quote email sent from now on goes out under the new sender identity.");
224
+ if ("default_billing_provider" in changes) {
225
+ out.push(changes.default_billing_provider === "stripe"
226
+ ? "Quotes signed from now on create a Stripe customer, and a subscription or invoice, in the connected Stripe account."
227
+ : "Quotes signed from now on create no invoice or subscription: finance bills them elsewhere.");
228
+ }
229
+ if ("auto_close_deal_on_acceptance" in changes) out.push(changes.auto_close_deal_on_acceptance ? "An accepted quote moves its deal to Closed Won." : "An accepted quote no longer moves its deal to Closed Won.");
230
+ if ("auto_advance_deal_on_quote_send" in changes) out.push(changes.auto_advance_deal_on_quote_send ? "Sending a quote moves an earlier open deal to Proposal Sent." : "Sending a quote no longer moves the deal to Proposal Sent.");
231
+ void was;
232
+ return out;
233
+ }
234
+
235
+ // ---------------------------------------------------------------------------
236
+ // Batch actions
237
+ // ---------------------------------------------------------------------------
238
+
239
+ export type BatchAction = "archive" | "expire" | "revoke";
240
+
241
+ const NOT_EXPIRABLE = new Set(["draft", "accepted", "expired", "declined", "voided", "completed", "archived"]);
242
+ const REVOCABLE = new Set(["sent", "viewed", "link_generated"]);
243
+
244
+ /**
245
+ * Whether a batch action applies to a quote with this status (the same rules the API applies row by row, so a quote it
246
+ * would only skip is left out of the request and out of the confirm count). Archive: not already archived. Expire: not a
247
+ * draft or already final. Revoke: out for signature (sent, viewed or link generated).
248
+ */
249
+ export function batchEligible(action: BatchAction, row: Rec): boolean {
250
+ const status = text(row.status);
251
+ if (action === "archive") return status !== "archived" && !row.archived_at && row.is_archived !== true && row.archived !== true;
252
+ if (action === "expire") return status !== "" && !NOT_EXPIRABLE.has(status);
253
+ return REVOCABLE.has(status);
254
+ }
255
+
256
+ export interface BatchOutcome { quote_id: string; status: string; detail?: string }
257
+
258
+ /** Read the per-row answer of POST /quotes/batch. */
259
+ export function batchOutcomes(res: unknown): BatchOutcome[] {
260
+ const data = asRec(asRec(res).data ?? res);
261
+ return (Array.isArray(data.results) ? data.results : []).map((r) => ({ quote_id: text(asRec(r).quote_id), status: text(asRec(r).status), detail: text(asRec(r).detail) || undefined }));
262
+ }
263
+
264
+ export const BATCH_REASON: Record<string, string> = { not_found: "no longer exists", forbidden: "you cannot change it", invalid_id: "not a valid id" };
@@ -20,6 +20,37 @@ deep-linkable (`?id=` the sequence, `?dtab=` the tab, `?step=` a step, `?enrollm
20
20
  - **`SequenceEnrollments`**: each enrolled contact is a record (state, step, the full contact); pause directly,
21
21
  resume confirmed.
22
22
 
23
+ **Managing a sequence (new).** Everything a sequence needs after it is built is reachable from the screen:
24
+
25
+ - **Edit** (`SequenceEditForm`, in the detail's action row): rename it and change its description, stop-on-reply,
26
+ same-thread and pick-up-new-contacts settings. The form loads the sequence first, saves only the fields you changed
27
+ (a partial `PATCH`), and confirms (**external**: a live sequence's contacts are affected). The audience list cannot be
28
+ changed after creation (the API has no way).
29
+ - **Sync audience**: re-pulls the audience list so contacts added since launch are enrolled (**external**, confirmed;
30
+ shown for live and paused sequences).
31
+ - **Delete sequence**: **destructive**, confirmed. The API's `DELETE` is a soft delete identical to archive (the sequence
32
+ stops and is hidden, and cannot be restored through the API); the Archive button stays for backwards compatibility.
33
+ - **Senders** tab (`SequenceSendersPanel`): the mailboxes, numbers and LinkedIn accounts the sequence sends from, with
34
+ remove (**destructive**, confirmed; the dialog says when it is the only sender of that kind).
35
+ - **Remove from sequence** on an enrolled contact (`WithdrawContactButton`): takes one contact out of this sequence,
36
+ recording removed / completed / not interested (**destructive**, confirmed; never "all sequences").
37
+ - **Templates** (header button, `SequenceTemplates` and `StepTemplates`): sequence templates (list, create, edit, archive,
38
+ restore) and step templates (the same, plus an AI draft of a batch: **billable**, confirmed, nothing saved until you keep
39
+ one). **Use template** opens the new-sequence sheet with the template's name and steps filled in; you add the audience
40
+ list and senders. In the builder, **From a step template** inserts one step. **Save as template** (detail) copies a
41
+ sequence's steps. A template is a copy: it never changes a sequence built from it.
42
+ - **AI help on a step** (`StepAiAssist`, inside the builder's step form): improve the instructions of an AI-written email,
43
+ and preview a draft for one contact (email or LinkedIn). Every AI call is **billable** and confirmed; the email draft
44
+ shows the exact credit estimate (`estimateEmailDraft`, free) first. Nothing is sent: the draft is shown, and "Use as my
45
+ copy" writes it into the step as plain copy you can edit.
46
+
47
+ Not offered, with the reason: reordering the steps of a saved sequence (the API has no reorder operation and
48
+ `updateSequenceStep` has no order field; unsaved steps can still be moved); linking a sequence to a campaign
49
+ (`linkSequenceCampaign`, because `GET /sequences/{id}` does not return the current link, so the UI could not show it);
50
+ `setSequenceStatus` and `terminateSequence` (raw status writes; pause, resume, archive and delete already cover them);
51
+ `generateSequenceSpintax`, `placeVoiceTestCall` (rings a real phone), `getSequenceReports`, `getSequenceRoutingSummary`,
52
+ `listSequenceContactIds`, schedules, WhatsApp/SMS templates.
53
+
23
54
  Slot props: `toolbarActions`, `renderDetailActions(sequence)`, `extraDetailTabs`, `ownerEmail`. Needs a connected
24
55
  sender mailbox and a contact list (see Prerequisites). `SequenceList` gained an optional `onChanged`; `EnrollDialog`
25
56
  an optional `note`.
@@ -37,6 +68,13 @@ the enrollments page being shown (the API has no single-enrollment read), so a l
37
68
  | `StepEditor` | One step's form, with the rules the API only enforces at send time. | read |
38
69
  | `EnrollDialog` | Confirm and enroll contacts, with the recipient count and a deliverability warning. | read, external |
39
70
  | `SequenceStats` | Totals, per-step progress, skip reasons, per-contact state with pause and resume. | read, write, external |
71
+ | `SequenceEditForm` | Rename and edit a sequence's settings; loads first, sends changed fields only, confirmed. | read, external |
72
+ | `SequenceManageActions` | Edit, sync audience, save as template, delete. | read, write, external, destructive |
73
+ | `SequenceSendersPanel` | A sequence's senders, with remove. | read, destructive |
74
+ | `SequenceTemplates` | Sequence templates: list, create, edit, archive, restore, use. | read, write, destructive |
75
+ | `StepTemplates` (+ `StepTemplatePicker`) | Step templates: list, create, edit, archive, restore, AI drafts; insert one into a sequence. | read, write, billable, destructive |
76
+ | `StepAiAssist` | AI help for one step: improve instructions, preview a draft for a contact. | read, billable |
77
+ | `WithdrawContactButton` | Take one contact out of one sequence, recording the outcome. | write |
40
78
 
41
79
  Launch, pause, resume, enroll and step edits reach or change what real people receive, so each opens
42
80
  `ConfirmSpendDialog` first (with the recipient count where it applies). Archive is destructive and the API cannot
@@ -89,6 +127,8 @@ un-archive.
89
127
  ## Defaults (do not ask the human)
90
128
 
91
129
  - Two days between steps; stop on reply; the person writes the email copy.
130
+ - AI copy is opt-in and always previewed: nothing is drafted without a confirm that says it uses credits, and a draft is
131
+ never sent or saved until the person uses it.
92
132
  - **Ask the human**: who to enroll and when to launch. Never launch or enroll without approval of the count.
93
133
  - SMS and WhatsApp steps exist in the API (`SMS`, `WHATSAPP`, `message_body`) but this block's builder covers email, LinkedIn, call and call task.
94
134
 
@@ -124,6 +164,20 @@ un-archive.
124
164
  | `pause_sequence_contact_sequences__sequence_id__contacts__contact_id__pause_post` | POST /sequences/{sequence_id}/contacts/{contact_id}/pause | write | SequenceStats |
125
165
  | `resume_sequence_contact_sequences__sequence_id__contacts__contact_id__resume_post` | POST /sequences/{sequence_id}/contacts/{contact_id}/resume | external | SequenceStats |
126
166
 
167
+ | `update_sequence_sequences__sequence_id__patch` | PATCH /sequences/{sequence_id} | external | SequenceEditForm (changed fields only) |
168
+ | `delete_sequence_sequences__sequence_id__delete` | DELETE /sequences/{sequence_id} | destructive | SequenceManageActions |
169
+ | `sync_sequence_sequences__sequence_id__sync_post` | POST /sequences/{sequence_id}/sync | external | SequenceManageActions |
170
+ | `remove_sequence_channel_sequences__sequence_id__channels__sequence_channel_id__delete` | DELETE /sequences/{sequence_id}/channels/{sequence_channel_id} | destructive | SequenceSendersPanel |
171
+ | `list/create/update_sequence_template(s)`, `archive_sequence_template` | GET, POST /sequencer/templates/sequences; PATCH .../{id} and .../{id}/archive | read, write, destructive | SequenceTemplates, SequenceManageActions |
172
+ | `list/create/update_step_template(s)`, `archive_step_template` | GET, POST /sequencer/templates/steps; PATCH .../{id} and .../{id}/archive | read, write, destructive | StepTemplates |
173
+ | `generate_step_templates_sequencer_templates_steps_generate_post` | POST /sequencer/templates/steps/generate | billable | StepTemplates |
174
+ | `list_sequencer_textual_agents_sequencer_agents_textual_get` | GET /sequencer/agents/textual | read | StepTemplates, StepAiAssist |
175
+ | `improve_step_instruction_sequencer_content_instructions_improve_post` | POST /sequencer/content/instructions/improve | billable | StepAiAssist |
176
+ | `estimate_email_draft_sequencer_content_email_estimate_post` | POST /sequencer/content/email/estimate | read (free) | StepAiAssist |
177
+ | `draft_email_sequencer_content_email_generate_post` | POST /sequencer/content/email/generate | billable | StepAiAssist |
178
+ | `draft_linkedin_message_sequencer_content_linkedin_generate_post` | POST /sequencer/content/linkedin/generate | billable | StepAiAssist |
179
+ | `withdraw_contacts_from_sequences_contacts_withdraw_from_sequences_post` | POST /contacts/withdraw-from-sequences | write | WithdrawContactButton |
180
+
127
181
  Recipes (`recipes/`): `create-multichannel-sequence.ts`, `launch-and-enroll.ts`, `sequence-report.ts`, `pause-when-unsafe.ts`.
128
182
 
129
183
  ## Cross-block glue
@@ -131,3 +185,4 @@ Recipes (`recipes/`): `create-multichannel-sequence.ts`, `launch-and-enroll.ts`,
131
185
  - `AddToSequenceButton` (`components/add-to-sequence-button.tsx`): "Add to sequence" for any selection of contacts (live sequences only, then the enroll dialog with its preflight and confirmation). `SequencesScreen` gains optional `onOpenContact`: an enrolled person gets an "Open in contacts" button.
132
186
 
133
187
  - `RecordSequencesTab` (`components/record-sequences-tab.tsx`): a contact's enrollments with state and step (`getContactSequences`, the API's 50 most recent, with a notice when full), `AddToSequenceButton`, and a confirmed Withdraw from one sequence (`withdrawContactsFromSequences`, needs `contacts:write`). `extraDetailTabs` entry for ContactsScreen.
188
+ - `SequenceCampaignLink` (`components/sequence-campaign-link.tsx`): which campaign a sequence belongs to. Shows the linked campaign with "Open campaign" and "Unlink campaign" (destructive confirm; both records are kept), or a picker to link one. Use it in `SequencesScreen.renderDetailActions`; the row it receives carries `campaign_builder_campaign_id`.
@@ -2,12 +2,16 @@
2
2
  "$schema": "../block.schema.json",
3
3
  "id": "sequencer",
4
4
  "title": "Sequencer",
5
- "summary": "Build multi-channel outreach sequences (email, LinkedIn, calls), launch and pause them, enroll contacts, and read performance.",
5
+ "summary": "Build multi-channel outreach sequences, launch, pause, rename and delete them, start from templates, get AI help with copy, enroll and withdraw contacts, and read performance.",
6
6
  "features": [
7
7
  "multi-channel step builder with send-time rule checks",
8
8
  "launch, pause, resume, duplicate and archive with recipient-count confirmation",
9
9
  "enroll contacts with deliverability warning",
10
- "per-step stats, skip reasons and per-contact control"
10
+ "per-step stats, skip reasons and per-contact control",
11
+ "rename, edit, sync and delete a sequence (confirmed)",
12
+ "sequence and step templates, and AI-drafted step templates (billable, confirmed)",
13
+ "AI help on a step: improve instructions, preview a draft for a contact (billable, confirmed)",
14
+ "remove a sender from a sequence, withdraw one contact"
11
15
  ],
12
16
  "dependsOn": [
13
17
  "lists",
@@ -18,15 +22,28 @@
18
22
  "add_sequence_channels_sequences__sequence_id__channels_post",
19
23
  "add_sequence_steps_sequences__sequence_id__steps_post",
20
24
  "archive_sequence_sequences__sequence_id__archive_post",
25
+ "archive_sequence_template_sequencer_templates_sequences__sequence_template_id__archive_patch",
26
+ "archive_step_template_sequencer_templates_steps__step_template_id__archive_patch",
21
27
  "create_sequence_sequences_post",
28
+ "create_sequence_template_sequencer_templates_sequences_post",
29
+ "create_step_template_sequencer_templates_steps_post",
30
+ "delete_sequence_sequences__sequence_id__delete",
22
31
  "delete_sequence_step_sequences__sequence_id__steps__step_id__delete",
32
+ "draft_email_sequencer_content_email_generate_post",
33
+ "draft_linkedin_message_sequencer_content_linkedin_generate_post",
23
34
  "duplicate_sequence_sequences__sequence_id__duplicate_post",
35
+ "estimate_email_draft_sequencer_content_email_estimate_post",
36
+ "generate_step_templates_sequencer_templates_steps_generate_post",
37
+ "get_campaign_campaigns__campaign_id__get",
24
38
  "get_contact_sequences_contacts__contact_id__sequences_get",
25
39
  "get_outreach_connection_integrations_linkedin_outreach_connection_get",
26
40
  "get_safe_to_send_deliverability_safe_to_send_get",
27
41
  "get_sequence_analytics_sequences__sequence_id__analytics_get",
28
42
  "get_sequence_sequences__sequence_id__get",
29
43
  "get_sequence_stats_sequences__sequence_id__stats_get",
44
+ "improve_step_instruction_sequencer_content_instructions_improve_post",
45
+ "link_sequence_campaign_sequences__sequence_id__campaign_put",
46
+ "list_campaigns_for_picker_campaigns_picker_get",
30
47
  "list_contacts_contacts_get",
31
48
  "list_linkedin_accounts_sequencer_channels_linkedin_accounts_get",
32
49
  "list_lists_lists_get",
@@ -34,15 +51,24 @@
34
51
  "list_sequence_channels_sequences__sequence_id__channels_get",
35
52
  "list_sequence_contacts_sequences__sequence_id__contacts_get",
36
53
  "list_sequence_steps_sequences__sequence_id__steps_get",
54
+ "list_sequence_templates_sequencer_templates_sequences_get",
37
55
  "list_sequencer_phone_numbers_sequencer_channels_phone_numbers_get",
56
+ "list_sequencer_textual_agents_sequencer_agents_textual_get",
38
57
  "list_sequences_sequences_get",
58
+ "list_step_templates_sequencer_templates_steps_get",
39
59
  "pause_sequence_contact_sequences__sequence_id__contacts__contact_id__pause_post",
40
60
  "pause_sequence_sequences__sequence_id__pause_post",
41
61
  "preview_sequence_sequences__sequence_id__preview_get",
62
+ "remove_sequence_channel_sequences__sequence_id__channels__sequence_channel_id__delete",
42
63
  "resume_sequence_contact_sequences__sequence_id__contacts__contact_id__resume_post",
43
64
  "resume_sequence_sequences__sequence_id__resume_post",
44
65
  "run_sequence_sequences__sequence_id__run_post",
66
+ "sync_sequence_sequences__sequence_id__sync_post",
67
+ "unlink_sequence_campaign_sequences__sequence_id__campaign_delete",
68
+ "update_sequence_sequences__sequence_id__patch",
45
69
  "update_sequence_step_sequences__sequence_id__steps__step_id__patch",
70
+ "update_sequence_template_sequencer_templates_sequences__sequence_template_id__patch",
71
+ "update_step_template_sequencer_templates_steps__step_template_id__patch",
46
72
  "withdraw_contacts_from_sequences_contacts_withdraw_from_sequences_post"
47
73
  ],
48
74
  "components": [
@@ -94,7 +120,7 @@
94
120
  {
95
121
  "name": "StepEditor",
96
122
  "file": "components/step-editor.tsx",
97
- "purpose": "Controlled form for one step with the rules the API only enforces at send time; shows LinkedIn sender readiness.",
123
+ "purpose": "Controlled form for one step with the rules the API only enforces at send time; shows LinkedIn sender readiness. Optional renderAssist slot for AI help.",
98
124
  "operations": [
99
125
  "get_outreach_connection_integrations_linkedin_outreach_connection_get"
100
126
  ],
@@ -133,10 +159,116 @@
133
159
  "external"
134
160
  ]
135
161
  },
162
+ {
163
+ "name": "SequenceEditForm",
164
+ "file": "components/sequence-edit-form.tsx",
165
+ "purpose": "Rename a sequence and change its settings (description, stop on reply, same thread, new contacts). Loads the record first, saves only the changed fields, and confirms before changing a sequence that may be live.",
166
+ "operations": [
167
+ "get_sequence_sequences__sequence_id__get",
168
+ "update_sequence_sequences__sequence_id__patch"
169
+ ],
170
+ "tiers": [
171
+ "read",
172
+ "external"
173
+ ]
174
+ },
175
+ {
176
+ "name": "SequenceManageActions",
177
+ "file": "components/sequence-manage-actions.tsx",
178
+ "purpose": "Edit, sync the audience list (confirmed), save as template and delete (confirmed) for one sequence.",
179
+ "operations": [
180
+ "create_sequence_template_sequencer_templates_sequences_post",
181
+ "delete_sequence_sequences__sequence_id__delete",
182
+ "list_sequence_steps_sequences__sequence_id__steps_get",
183
+ "sync_sequence_sequences__sequence_id__sync_post"
184
+ ],
185
+ "tiers": [
186
+ "read",
187
+ "write",
188
+ "external",
189
+ "destructive"
190
+ ]
191
+ },
192
+ {
193
+ "name": "SequenceSendersPanel",
194
+ "file": "components/sequence-senders-panel.tsx",
195
+ "purpose": "The senders (mailboxes, numbers, LinkedIn accounts) a sequence sends from, with remove (confirmed).",
196
+ "operations": [
197
+ "list_sequence_channels_sequences__sequence_id__channels_get",
198
+ "remove_sequence_channel_sequences__sequence_id__channels__sequence_channel_id__delete"
199
+ ],
200
+ "tiers": [
201
+ "read",
202
+ "destructive"
203
+ ]
204
+ },
205
+ {
206
+ "name": "SequenceTemplates",
207
+ "file": "components/sequence-templates.tsx",
208
+ "purpose": "Sequence templates: list, create, edit (only changes are sent), archive (confirmed) and restore, and use one to start a new sequence.",
209
+ "operations": [
210
+ "archive_sequence_template_sequencer_templates_sequences__sequence_template_id__archive_patch",
211
+ "create_sequence_template_sequencer_templates_sequences_post",
212
+ "list_sequence_templates_sequencer_templates_sequences_get",
213
+ "update_sequence_template_sequencer_templates_sequences__sequence_template_id__patch"
214
+ ],
215
+ "tiers": [
216
+ "read",
217
+ "write",
218
+ "destructive"
219
+ ]
220
+ },
221
+ {
222
+ "name": "StepTemplates",
223
+ "file": "components/step-templates.tsx",
224
+ "purpose": "Step templates (also exports StepTemplatePicker): list, create, edit, archive (confirmed), restore, and have an agent draft a batch (billable, confirmed).",
225
+ "operations": [
226
+ "archive_step_template_sequencer_templates_steps__step_template_id__archive_patch",
227
+ "create_step_template_sequencer_templates_steps_post",
228
+ "generate_step_templates_sequencer_templates_steps_generate_post",
229
+ "list_sequencer_textual_agents_sequencer_agents_textual_get",
230
+ "list_step_templates_sequencer_templates_steps_get",
231
+ "update_step_template_sequencer_templates_steps__step_template_id__patch"
232
+ ],
233
+ "tiers": [
234
+ "read",
235
+ "write",
236
+ "billable",
237
+ "destructive"
238
+ ]
239
+ },
240
+ {
241
+ "name": "StepAiAssist",
242
+ "file": "components/step-ai-assist.tsx",
243
+ "purpose": "AI help for one email or LinkedIn step: improve the instructions and preview a draft for one contact; each AI call is billable and confirmed (the email draft shows its credit estimate), nothing is sent.",
244
+ "operations": [
245
+ "draft_email_sequencer_content_email_generate_post",
246
+ "draft_linkedin_message_sequencer_content_linkedin_generate_post",
247
+ "estimate_email_draft_sequencer_content_email_estimate_post",
248
+ "improve_step_instruction_sequencer_content_instructions_improve_post",
249
+ "list_contacts_contacts_get",
250
+ "list_sequencer_textual_agents_sequencer_agents_textual_get"
251
+ ],
252
+ "tiers": [
253
+ "read",
254
+ "billable"
255
+ ]
256
+ },
257
+ {
258
+ "name": "WithdrawContactButton",
259
+ "file": "components/enrollment-withdraw.tsx",
260
+ "purpose": "Take one contact out of one sequence, recording the outcome (removed, completed, not interested); confirmed.",
261
+ "operations": [
262
+ "withdraw_contacts_from_sequences_contacts_withdraw_from_sequences_post"
263
+ ],
264
+ "tiers": [
265
+ "write"
266
+ ]
267
+ },
136
268
  {
137
269
  "name": "SequencesScreen",
138
270
  "file": "components/sequences-screen.tsx",
139
- "purpose": "Finished sequences page: status stats and filter, list with launch/pause/resume, new-sequence sheet, detail with results, steps and enroll-contacts.",
271
+ "purpose": "Finished sequences page: status stats and filter, list with launch/pause/resume, new-sequence sheet, templates, detail with results, steps, senders, edit, sync, delete and enroll-contacts.",
140
272
  "operations": [
141
273
  "list_sequences_sequences_get"
142
274
  ],
@@ -147,7 +279,7 @@
147
279
  {
148
280
  "name": "SequenceDetail",
149
281
  "file": "components/sequence-detail.tsx",
150
- "purpose": "One sequence, complete: every field, tabs for steps, contacts and results; launch (refused with the reasons when a step has no sender or the copy would block it), pause, resume, duplicate, archive and enroll (confirmed).",
282
+ "purpose": "One sequence, complete: every field, tabs for steps, contacts and results; launch (refused with the reasons when a step has no sender or the copy would block it), pause, resume, duplicate, archive and enroll (confirmed). Also edit, sync audience, save as template, delete and a Senders tab.",
151
283
  "operations": [
152
284
  "get_sequence_sequences__sequence_id__get",
153
285
  "preview_sequence_sequences__sequence_id__preview_get",
@@ -215,6 +347,22 @@
215
347
  "read",
216
348
  "write"
217
349
  ]
350
+ },
351
+ {
352
+ "name": "SequenceCampaignLink",
353
+ "file": "components/sequence-campaign-link.tsx",
354
+ "purpose": "Which campaign a sequence belongs to: the linked campaign with an Open campaign button and Unlink, or a picker to link one. Drop into SequencesScreen.renderDetailActions.",
355
+ "operations": [
356
+ "get_campaign_campaigns__campaign_id__get",
357
+ "list_campaigns_for_picker_campaigns_picker_get",
358
+ "link_sequence_campaign_sequences__sequence_id__campaign_put",
359
+ "unlink_sequence_campaign_sequences__sequence_id__campaign_delete"
360
+ ],
361
+ "tiers": [
362
+ "read",
363
+ "write",
364
+ "destructive"
365
+ ]
218
366
  }
219
367
  ],
220
368
  "hooks": [],
@@ -222,6 +370,10 @@
222
370
  {
223
371
  "file": "lib/sequencer-utils.ts",
224
372
  "purpose": "Step kinds and the draft-to-API mapping, send-time rule checks, status helpers and envelope readers."
373
+ },
374
+ {
375
+ "file": "lib/sequencer-manage.ts",
376
+ "purpose": "Edit-only-what-changed diffs, sequence and step template readers and bodies, contact and agent readers for AI drafting."
225
377
  }
226
378
  ],
227
379
  "recipes": [
@@ -312,6 +464,21 @@
312
464
  "decision": "Audience list",
313
465
  "default": "Required when creating a draft (the API cannot set it later, and a sequence without one cannot launch)",
314
466
  "askHuman": false
467
+ },
468
+ {
469
+ "decision": "Deleting versus archiving a sequence",
470
+ "default": "Both are the API's soft delete (the sequence stops and is hidden, and cannot be restored through the API). Delete and Archive are both offered and both confirmed",
471
+ "askHuman": false
472
+ },
473
+ {
474
+ "decision": "Reordering the steps of a saved sequence",
475
+ "default": "Not offered: the API has no reorder operation and a saved step has no order field to patch. Unsaved steps can be moved before they are added",
476
+ "askHuman": false
477
+ },
478
+ {
479
+ "decision": "Who may spend credits on AI drafts",
480
+ "default": "Every AI call (improve instructions, preview a draft, draft step templates) goes through a confirm dialog that says it uses credits; the email draft shows the exact estimate first",
481
+ "askHuman": false
315
482
  }
316
483
  ],
317
484
  "peerDependencies": {