@ecodrix/erix-api 1.2.0 → 1.2.1

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ecodrix/erix-api",
3
- "version": "1.2.0",
3
+ "version": "1.2.1",
4
4
  "author": "ECODrIx Team <contact@ecodrix.com>",
5
5
  "license": "MIT",
6
6
  "description": "Official Isomorphic SDK for the ECODrIx platform. Native support for WhatsApp, CRM, Storage, and Meetings across TS, JS, Python, and Java.",
package/src/core.ts CHANGED
@@ -3,11 +3,12 @@ import axiosRetry from "axios-retry";
3
3
  import { type Socket, io } from "socket.io-client";
4
4
  import { APIError, AuthenticationError } from "./error";
5
5
  import { CRM } from "./resources/crm";
6
- import { EmailResource } from "./resources/email";
6
+ import { Email } from "./resources/email";
7
7
  import { EventsResource } from "./resources/events";
8
8
  import { Health } from "./resources/health";
9
+ import { Logs } from "./resources/logs";
9
10
  import { Marketing } from "./resources/marketing";
10
- import { MediaResource } from "./resources/media";
11
+ import { Media } from "./resources/media";
11
12
  import { Meetings } from "./resources/meet";
12
13
  import { Notifications } from "./resources/notifications";
13
14
  import { Queue } from "./resources/queue";
@@ -90,7 +91,7 @@ export class Ecodrix {
90
91
  public readonly crm: CRM;
91
92
 
92
93
  /** Cloudflare R2-backed media storage. */
93
- public readonly media: MediaResource;
94
+ public readonly media: Media;
94
95
 
95
96
  /** Google Meet appointment scheduling. */
96
97
  public readonly meet: Meetings;
@@ -98,8 +99,11 @@ export class Ecodrix {
98
99
  /** Automation execution logs and provider webhook callbacks. */
99
100
  public readonly notifications: Notifications;
100
101
 
101
- /** Outbound email marketing engine. */
102
- public readonly email: EmailResource;
102
+ /** Outbound email marketing engine and template management. */
103
+ public readonly email: Email;
104
+
105
+ /** Platform-wide execution logs and audit trails. */
106
+ public readonly logs: Logs;
103
107
 
104
108
  /** Lead events and workflow automation triggers. */
105
109
  public readonly events: EventsResource;
@@ -129,8 +133,7 @@ export class Ecodrix {
129
133
  const baseUrl = options.baseUrl ?? ECOD_API_BASE;
130
134
  const socketUrl = options.socketUrl || baseUrl;
131
135
 
132
- const isBrowser =
133
- typeof window !== "undefined" && typeof window.document !== "undefined";
136
+ const isBrowser = typeof window !== "undefined" && typeof window.document !== "undefined";
134
137
  const runtime = isBrowser
135
138
  ? "browser"
136
139
  : typeof process !== "undefined"
@@ -163,14 +166,11 @@ export class Ecodrix {
163
166
  retryDelay: axiosRetry.exponentialDelay,
164
167
  retryCondition: (error) => {
165
168
  return (
166
- axiosRetry.isNetworkOrIdempotentRequestError(error) ||
167
- error.response?.status === 429
169
+ axiosRetry.isNetworkOrIdempotentRequestError(error) || error.response?.status === 429
168
170
  );
169
171
  },
170
172
  onRetry: (retryCount, error, requestConfig) => {
171
- const isDev =
172
- typeof process !== "undefined" &&
173
- process.env?.NODE_ENV === "development";
173
+ const isDev = typeof process !== "undefined" && process.env?.NODE_ENV === "development";
174
174
  if (isDev) {
175
175
  console.warn(
176
176
  `[ECODrIx SDK] Retrying request (${retryCount}/3): ${requestConfig.method?.toUpperCase()} ${requestConfig.url}. Reason: ${error.message}`,
@@ -182,10 +182,11 @@ export class Ecodrix {
182
182
  // Initialise resources
183
183
  this.whatsapp = new WhatsApp(this.client);
184
184
  this.crm = new CRM(this.client);
185
- this.media = new MediaResource(this.client);
185
+ this.media = new Media(this.client);
186
186
  this.meet = new Meetings(this.client);
187
187
  this.notifications = new Notifications(this.client);
188
- this.email = new EmailResource(this.client);
188
+ this.email = new Email(this.client);
189
+ this.logs = new Logs(this.client);
189
190
  this.events = new EventsResource(this.client);
190
191
  this.webhooks = new Webhooks();
191
192
  this.storage = new Storage(this.client);
@@ -323,9 +324,7 @@ export class Ecodrix {
323
324
  } catch (error: any) {
324
325
  if (error.response) {
325
326
  throw new APIError(
326
- error.response.data?.message ||
327
- error.response.data?.error ||
328
- "Raw Execution Failed",
327
+ error.response.data?.message || error.response.data?.error || "Raw Execution Failed",
329
328
  error.response.status,
330
329
  error.response.data?.code,
331
330
  );
package/src/index.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export { Ecodrix, type EcodrixOptions } from "./core";
2
+ export * from "./types/metadata";
2
3
  export * from "./error";
3
4
  export * from "./resources/crm/activities";
4
5
  export * from "./resources/crm/analytics";
@@ -13,6 +14,7 @@ export * from "./resources/crm/sequences";
13
14
  export * from "./resources/email";
14
15
  export * from "./resources/events";
15
16
  export * from "./resources/health";
17
+ export * from "./resources/logs";
16
18
  export * from "./resources/marketing";
17
19
  export * from "./resources/media";
18
20
  export * from "./resources/meet";
@@ -1,4 +1,5 @@
1
1
  import { APIResource } from "../../resource";
2
+ import type { ResourceManifest } from "../../types/metadata";
2
3
 
3
4
  /**
4
5
  * Valid statuses for a CRM Lead.
@@ -102,6 +103,69 @@ export class Leads extends APIResource {
102
103
  return this.post<T>("/api/crm/leads", params);
103
104
  }
104
105
 
106
+ /**
107
+ * Introspect the Leads resource and provide a "Blueprint" for UI generation.
108
+ *
109
+ * This method returns a list of all fields (standard + custom), their types,
110
+ * labels, and suggested UI controls. Erix React uses this to render SmartForms
111
+ * and tables dynamically.
112
+ */
113
+ async describe(): Promise<ResourceManifest> {
114
+ // 1. Fetch custom fields from the tenant configuration
115
+ const customFieldsRes: any = await this.fields();
116
+ const customFields = Array.isArray(customFieldsRes.data) ? customFieldsRes.data : [];
117
+
118
+ // 2. Define the core system fields
119
+ const systemFields: any[] = [
120
+ {
121
+ key: "firstName",
122
+ label: "First Name",
123
+ type: "string",
124
+ required: true,
125
+ group: "Basic Info",
126
+ },
127
+ { key: "lastName", label: "Last Name", type: "string", required: false, group: "Basic Info" },
128
+ { key: "phone", label: "Phone Number", type: "phone", required: true, group: "Contact" },
129
+ { key: "email", label: "Email Address", type: "email", required: false, group: "Contact" },
130
+ {
131
+ key: "status",
132
+ label: "Status",
133
+ type: "select",
134
+ required: true,
135
+ options: [
136
+ { label: "New", value: "new" },
137
+ { label: "Contacted", value: "contacted" },
138
+ { label: "Qualified", value: "qualified" },
139
+ { label: "Won", value: "won" },
140
+ { label: "Lost", value: "lost" },
141
+ ],
142
+ },
143
+ { key: "value", label: "Lead Value", type: "currency", required: false },
144
+ { key: "source", label: "Source", type: "string", required: false },
145
+ ];
146
+
147
+ // 3. Map custom fields to FieldManifest shape
148
+ const mappedCustom = customFields.map((f: any) => ({
149
+ key: `metadata.extra.${f.name}`,
150
+ label: f.label || f.name,
151
+ type: f.type || "string",
152
+ required: !!f.required,
153
+ options: f.options,
154
+ group: "Custom Fields",
155
+ }));
156
+
157
+ return {
158
+ name: "Lead",
159
+ fields: [...systemFields, ...mappedCustom],
160
+ uiHints: {
161
+ icon: "User",
162
+ primaryColor: "#3b82f6",
163
+ defaultSort: { field: "createdAt", direction: "desc" },
164
+ summaryFields: ["firstName", "lastName", "phone", "status"],
165
+ },
166
+ };
167
+ }
168
+
105
169
  /**
106
170
  * Upsert a lead by phone number. If a lead with the same phone exists,
107
171
  * it will be updated; otherwise, a new lead is created.
@@ -1,4 +1,5 @@
1
1
  import { APIResource } from "../../resource";
2
+ import type { ResourceManifest } from "../../types/metadata";
2
3
 
3
4
  export interface CreatePipelineParams {
4
5
  name: string;
@@ -26,6 +27,39 @@ export class Pipelines extends APIResource {
26
27
  return this.get<T>("/api/crm/pipelines");
27
28
  }
28
29
 
30
+ /**
31
+ * Introspect a pipeline and provide a manifest of its stages.
32
+ *
33
+ * Returns metadata about stage colors, probabilities, and order.
34
+ * Erix React uses this to build kanban boards and stage pickers.
35
+ *
36
+ * @param pipelineId - The unique ID of the pipeline.
37
+ */
38
+ async getStageManifest(pipelineId: string): Promise<ResourceManifest> {
39
+ const pipeline: any = await this.retrieve(pipelineId);
40
+ const stages = Array.isArray(pipeline.data?.stages) ? pipeline.data.stages : [];
41
+
42
+ return {
43
+ name: `Pipeline: ${pipeline.data?.name || pipelineId}`,
44
+ fields: stages.map((s: any) => ({
45
+ key: s._id,
46
+ label: s.name,
47
+ type: "string", // Labels are strings
48
+ required: true,
49
+ options: [{ label: s.name, value: s._id }],
50
+ group: "Stages",
51
+ uiHints: {
52
+ color: s.color || "#cbd5e1",
53
+ probability: s.probability,
54
+ },
55
+ })) as any,
56
+ uiHints: {
57
+ icon: "Columns",
58
+ primaryColor: "#6366f1",
59
+ },
60
+ };
61
+ }
62
+
29
63
  /**
30
64
  * Create a new CRM sales or support pipeline.
31
65
  *
@@ -74,7 +108,9 @@ export class Pipelines extends APIResource {
74
108
  * Duplicate a pipeline with a new name.
75
109
  */
76
110
  async duplicate<T = any>(pipelineId: string, newName: string) {
77
- return this.post<T>(`/api/crm/pipelines/${pipelineId}/duplicate`, { newName });
111
+ return this.post<T>(`/api/crm/pipelines/${pipelineId}/duplicate`, {
112
+ newName,
113
+ });
78
114
  }
79
115
 
80
116
  /**
@@ -1,5 +1,3 @@
1
- import type { AxiosInstance } from "axios";
2
- import { APIError } from "../error";
3
1
  import { APIResource } from "../resource";
4
2
 
5
3
  /**
@@ -23,38 +21,122 @@ export interface CampaignResult {
23
21
  [key: string]: any;
24
22
  }
25
23
 
26
- export class EmailResource extends APIResource {
24
+ export interface EmailTemplate {
25
+ id: string;
26
+ name: string;
27
+ description?: string;
28
+ subject: string;
29
+ preheader?: string;
30
+ htmlBody: string;
31
+ textBody?: string;
32
+ category: "marketing" | "transactional" | "sequence";
33
+ status: "draft" | "published" | "archived";
34
+ type: "standard" | "layout";
35
+ layoutId?: string;
36
+ thumbnail?: string;
37
+ variableMapping?: any[];
38
+ createdAt: string;
39
+ updatedAt: string;
40
+ }
41
+
42
+ export interface CreateTemplateDTO {
43
+ name: string;
44
+ subject: string;
45
+ htmlBody: string;
46
+ description?: string;
47
+ category?: "marketing" | "transactional" | "sequence";
48
+ status?: "draft" | "published" | "archived";
49
+ variableMapping?: any[];
50
+ }
51
+
52
+ export interface TemplatePreviewResponse {
53
+ success: boolean;
54
+ data: {
55
+ subject: string;
56
+ html: string;
57
+ text?: string;
58
+ };
59
+ }
60
+
61
+ /**
62
+ * Outbound email marketing engine and template management.
63
+ *
64
+ * Access via `ecod.email`.
65
+ */
66
+ export class Email extends APIResource {
27
67
  /**
28
68
  * Send an HTML email campaign to a list of recipients.
29
- *
30
- * @param payload - The campaign details (recipients, subject, html).
31
- * @returns The dispatch result indicating success or failure.
32
- * @example
33
- * ```typescript
34
- * await erixClient.email.sendEmailCampaign({
35
- * recipients: ["user1@example.com", "user2@example.com"],
36
- * subject: "Monthly Newsletter",
37
- * html: "<h1>Hello!</h1><p>Check out our latest updates...</p>"
38
- * });
39
- * ```
40
- */
41
- async sendEmailCampaign(payload: SendCampaignPayload): Promise<CampaignResult> {
69
+ */
70
+ async sendCampaign(payload: SendCampaignPayload): Promise<CampaignResult> {
42
71
  return this.post<CampaignResult>("/api/saas/emails/campaign", payload);
43
72
  }
44
73
 
45
74
  /**
46
75
  * Send a system test email to validate your current SMTP configuration.
47
- *
48
- * @param to - The recipient's email address.
49
- * @returns The dispatch result.
50
- * @example
51
- * ```typescript
52
- * await erixClient.email.sendTestEmail("admin@company.com");
53
- * ```
54
- */
55
- async sendTestEmail(to: string): Promise<CampaignResult> {
76
+ */
77
+ async sendTest(to: string): Promise<CampaignResult> {
56
78
  return this.post<CampaignResult>("/api/saas/emails/test", { to });
57
79
  }
58
80
 
59
- // WhatsApp moved to native wrapper
81
+ // --- Template Management ---
82
+
83
+ /**
84
+ * List all email templates.
85
+ */
86
+ async listTemplates(query?: any): Promise<{ success: boolean; data: EmailTemplate[] }> {
87
+ return this.get<{ success: boolean; data: EmailTemplate[] }>("/api/saas/mail/templates", {
88
+ params: query,
89
+ });
90
+ }
91
+
92
+ /**
93
+ * Get a single email template by ID.
94
+ */
95
+ async getTemplate(id: string): Promise<{ success: boolean; data: EmailTemplate }> {
96
+ return this.get<{ success: boolean; data: EmailTemplate }>(`/api/saas/mail/templates/${id}`);
97
+ }
98
+
99
+ /**
100
+ * Create a new email template.
101
+ */
102
+ async createTemplate(
103
+ data: CreateTemplateDTO,
104
+ ): Promise<{ success: boolean; data: EmailTemplate }> {
105
+ return this.post<{ success: boolean; data: EmailTemplate }>("/api/saas/mail/templates", data);
106
+ }
107
+
108
+ /**
109
+ * Update an existing email template.
110
+ */
111
+ async updateTemplate(
112
+ id: string,
113
+ data: Partial<CreateTemplateDTO>,
114
+ ): Promise<{ success: boolean; data: EmailTemplate }> {
115
+ return this.put<{ success: boolean; data: EmailTemplate }>(
116
+ `/api/saas/mail/templates/${id}`,
117
+ data,
118
+ );
119
+ }
120
+
121
+ /**
122
+ * Delete an email template.
123
+ */
124
+ async deleteTemplate(id: string, force = false): Promise<{ success: boolean }> {
125
+ return this.deleteRequest<{ success: boolean }>(`/api/saas/mail/templates/${id}`, {
126
+ params: { force },
127
+ });
128
+ }
129
+
130
+ /**
131
+ * Preview a template with dynamic variable resolution.
132
+ */
133
+ async previewTemplate(
134
+ id: string,
135
+ params: { leadId?: string; variables?: Record<string, any> },
136
+ ): Promise<TemplatePreviewResponse> {
137
+ return this.post<TemplatePreviewResponse>(`/api/saas/mail/templates/${id}/preview`, params);
138
+ }
60
139
  }
140
+
141
+ /** @deprecated Use Email instead */
142
+ export class EmailResource extends Email {}
@@ -34,48 +34,51 @@ export interface JobStatus {
34
34
  createdAt: string;
35
35
  }
36
36
 
37
+ /**
38
+ * Platform and tenant-specific health diagnostics.
39
+ *
40
+ * Access via `ecod.health`.
41
+ */
37
42
  export class Health extends APIResource {
38
43
  /**
39
44
  * Perform a global platform health check.
40
- * Verify that the API, database, and background workers are operational.
41
- *
42
- * @returns System health summary (status, version, uptime).
43
- * @example
44
- * ```typescript
45
- * const health = await erixClient.health.system();
46
- * console.log(`API Status: ${health.status}`);
47
- * ```
45
+ * Verify that the core API, database, and background workers are operational.
48
46
  */
49
47
  async system(): Promise<SystemHealth> {
50
- const res = await this.get<{ data: SystemHealth }>("/api/saas/health", {
51
- headers: { accept: "application/json" },
52
- });
48
+ const res = await this.get<{ success: boolean; data: SystemHealth }>("/api/saas/health");
53
49
  return res.data;
54
50
  }
55
51
 
56
52
  /**
57
- * Tenant-specific health check. Identifies configured services.
53
+ * Tenant-specific health check.
54
+ *
55
+ * Identifies correctly configured third-party integrations (WhatsApp, SMTP, Google Meet)
56
+ * and reports active automation counts.
58
57
  */
59
58
  async clientHealth(): Promise<ClientHealth> {
60
- const res = await this.get<{ data: ClientHealth }>("/api/saas/health/client");
59
+ const res = await this.get<{ success: boolean; data: ClientHealth }>("/api/saas/health/client");
61
60
  return res.data;
62
61
  }
63
62
 
63
+ /**
64
+ * Alias for clientHealth to match diagnostic terminology.
65
+ */
66
+ async getDiagnosticReport(): Promise<ClientHealth> {
67
+ return this.clientHealth();
68
+ }
69
+
64
70
  /**
65
71
  * Lookup the execution status of a background job.
66
72
  *
73
+ * Use this to poll for completion of long-running tasks like bulk exports
74
+ * or campaign dispatches.
75
+ *
67
76
  * @param jobId - The unique ID of the background job.
68
- * @returns Job status report (attempts, error trace, completion time).
69
- * @example
70
- * ```typescript
71
- * const status = await erixClient.health.jobStatus("job_123");
72
- * if (status.status === "completed") {
73
- * console.log("Job finished successfully!");
74
- * }
75
- * ```
76
77
  */
77
78
  async jobStatus(jobId: string): Promise<JobStatus> {
78
- const res = await this.get<{ data: JobStatus }>(`/api/saas/jobs/status/${jobId}`);
79
+ const res = await this.get<{ success: boolean; data: JobStatus }>(
80
+ `/api/saas/jobs/status/${jobId}`,
81
+ );
79
82
  return res.data;
80
83
  }
81
84
  }
@@ -0,0 +1,97 @@
1
+ import { APIResource } from "../resource";
2
+
3
+ export interface LogPaginationQuery {
4
+ page?: number;
5
+ limit?: number;
6
+ trigger?: string;
7
+ status?: "received" | "processing" | "completed" | "partial" | "failed";
8
+ from?: string;
9
+ to?: string;
10
+ }
11
+
12
+ export interface EventLog {
13
+ id: string;
14
+ trigger: string;
15
+ phone?: string;
16
+ email?: string;
17
+ status: "received" | "processing" | "completed" | "partial" | "failed";
18
+ rulesMatched: number;
19
+ jobsCreated: number;
20
+ meetLink?: string;
21
+ callbackUrl?: string;
22
+ callbackStatus: "not_required" | "sent" | "failed";
23
+ payload: any;
24
+ error?: string;
25
+ idempotencyKey?: string;
26
+ processedAt?: string;
27
+ createdAt: string;
28
+ updatedAt: string;
29
+ }
30
+
31
+ export interface CallbackLog {
32
+ id: string;
33
+ callbackUrl: string;
34
+ method: string;
35
+ payload: any;
36
+ jobId?: string;
37
+ enrollmentId?: string;
38
+ responseStatus: number;
39
+ responseBody: string;
40
+ status: "sent" | "failed" | "pending_retry";
41
+ attempts: number;
42
+ lastAttemptAt?: string;
43
+ signature?: string;
44
+ createdAt: string;
45
+ }
46
+
47
+ export interface LogResponse<T> {
48
+ success: boolean;
49
+ data: T[];
50
+ pagination: {
51
+ total: number;
52
+ page: number;
53
+ limit: number;
54
+ pages: number;
55
+ };
56
+ }
57
+
58
+ /**
59
+ * Audit and execution logs for platform events and webhooks.
60
+ *
61
+ * Access via `ecod.logs`.
62
+ */
63
+ export class Logs extends APIResource {
64
+ /**
65
+ * List event execution logs with optional filtering.
66
+ *
67
+ * Use this to audit automation triggers and debug workflow execution.
68
+ */
69
+ async listEventLogs(query?: LogPaginationQuery): Promise<LogResponse<EventLog>> {
70
+ return this.get<LogResponse<EventLog>>("/api/saas/events/logs", {
71
+ params: query,
72
+ });
73
+ }
74
+
75
+ /**
76
+ * Get detailed information for a single event execution.
77
+ */
78
+ async getEventLog(id: string): Promise<{ success: boolean; data: EventLog }> {
79
+ return this.get<{ success: boolean; data: EventLog }>(`/api/saas/events/logs/${id}`);
80
+ }
81
+
82
+ /**
83
+ * List webhook callback logs (outbound responses from ECODrIx to your system).
84
+ */
85
+ async listCallbackLogs(query?: LogPaginationQuery): Promise<LogResponse<CallbackLog>> {
86
+ return this.get<LogResponse<CallbackLog>>("/api/saas/callbacks/logs", {
87
+ params: query,
88
+ });
89
+ }
90
+
91
+ /**
92
+ * Get high-level execution statistics for the tenant.
93
+ */
94
+ async getStats(): Promise<{ success: boolean; data: any }> {
95
+ return this.get<{ success: boolean; data: any }>("/api/saas/events/stats");
96
+ }
97
+ }
@@ -41,7 +41,7 @@ export interface UploadOptions {
41
41
  * console.log(data.url); // → https://cdn.ecodrix.com/invoices/invoice-001.pdf
42
42
  * ```
43
43
  */
44
- export class MediaResource extends APIResource {
44
+ export class Media extends APIResource {
45
45
  /**
46
46
  * Get current storage usage metrics for the tenant.
47
47
  *
@@ -83,10 +83,7 @@ export class MediaResource extends APIResource {
83
83
  * const { data: files } = await ecod.media.list("invoices", { q: "contract" });
84
84
  * ```
85
85
  */
86
- async list(
87
- folder: string,
88
- params?: { year?: string; month?: string; q?: string },
89
- ) {
86
+ async list(folder: string, params?: { year?: string; month?: string; q?: string }) {
90
87
  return this.get(`/api/saas/storage/files/${folder}`, { params } as any);
91
88
  }
92
89
 
@@ -158,10 +155,7 @@ export class MediaResource extends APIResource {
158
155
  */
159
156
  async upload(file: any, options: UploadOptions): Promise<any> {
160
157
  // Step 1: Get presigned URL
161
- const response = await this.post<any>(
162
- "/api/saas/storage/upload-url",
163
- options,
164
- );
158
+ const response = await this.post<any>("/api/saas/storage/upload-url", options);
165
159
  const { uploadUrl, key } = response.data;
166
160
 
167
161
  // Step 2: Upload directly to R2 (bypasses the API server for performance)
@@ -175,3 +169,6 @@ export class MediaResource extends APIResource {
175
169
  return this.post("/api/saas/storage/confirm-upload", { key, sizeBytes });
176
170
  }
177
171
  }
172
+
173
+ /** @deprecated Use Media instead */
174
+ export class MediaResource extends Media {}
@@ -1,4 +1,5 @@
1
1
  import { APIResource } from "../../resource";
2
+ import type { ResourceManifest } from "../../types/metadata";
2
3
 
3
4
  export interface TemplateMapping {
4
5
  mappings: any[];
@@ -164,6 +165,50 @@ export class Templates extends APIResource {
164
165
  });
165
166
  }
166
167
 
168
+ /**
169
+ * Introspect a template and provide a manifest of its required variables.
170
+ *
171
+ * This is used by Erix React to build dynamic "Mapping Forms" where users
172
+ * can connect CRM fields to WhatsApp variables.
173
+ *
174
+ * @param templateIdentifier - The name or ID of the template.
175
+ */
176
+ async getVariableManifest(templateIdentifier: string): Promise<ResourceManifest> {
177
+ const template: any = await this.retrieve(templateIdentifier);
178
+ const components = Array.isArray(template.data?.components) ? template.data.components : [];
179
+
180
+ const fields: any[] = [];
181
+
182
+ for (const comp of components) {
183
+ // Look for {{n}} pattern in text components
184
+ if (comp.text) {
185
+ const vars = comp.text.match(/{{(\d+)}}/g);
186
+ if (vars) {
187
+ for (const v of vars) {
188
+ const index = v.replace(/{{|}}/g, "");
189
+ fields.push({
190
+ key: `var_${index}`,
191
+ label: `${comp.type} Var {{${index}}}`,
192
+ type: "string",
193
+ required: true,
194
+ group: comp.type,
195
+ description: `Variable placeholder in template ${comp.type}`,
196
+ });
197
+ }
198
+ }
199
+ }
200
+ }
201
+
202
+ return {
203
+ name: `Template variables: ${template.data?.name || templateIdentifier}`,
204
+ fields,
205
+ uiHints: {
206
+ icon: "Variables",
207
+ primaryColor: "#059669",
208
+ },
209
+ };
210
+ }
211
+
167
212
  /**
168
213
  * Check which automations or sequences are currently using this template.
169
214
  */