@ema.co/mcp-toolkit 2026.2.27-1 → 2026.2.27-2

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.

Potentially problematic release.


This version of @ema.co/mcp-toolkit might be problematic. Click here for more details.

@@ -33,8 +33,8 @@ export function formatSendResponse(result) {
33
33
  response._next_step = `conversation(method="send", conversation_id="${result.conversationId}", buttons={selected_button:{label:"<button label>"}})`;
34
34
  }
35
35
  else if (msgType.includes("FORM")) {
36
- response._tip = "The AI responded with a form. Fill it out using the form parameter, or cancel with is_cancelled=true.";
37
- response._next_step = `conversation(method="send", conversation_id="${result.conversationId}", form={fields:[{name:"<field>", value:{stringValue:"<value>"}}]})`;
36
+ response._tip = "The AI responded with a form. Echo back the entire formMessage with values filled in using raw_message. The form param only works for cancellation.";
37
+ response._next_step = `conversation(method="send", conversation_id="${result.conversationId}", original_message_id="<bot_msg_id>", raw_message={type:"MESSAGE_TYPE_FORM", formMessage:{...formMessage from response with values filled...}, isUserMessage:true})`;
38
38
  }
39
39
  else {
40
40
  response._tip = "Check message for the AI's reply. Use method='history' to review the full thread.";
@@ -77,7 +77,7 @@ export async function handleSend(args, client) {
77
77
  examples: [
78
78
  'text="hello" - send a text message',
79
79
  'buttons={selected_button:{label:"Yes",description:"Confirm"}} - select a button',
80
- 'form={fields:[{name:"email",value:{stringValue:"a@b.com"}}]} - submit a form',
80
+ 'raw_message={type:"MESSAGE_TYPE_FORM", formMessage:{...}} - submit a form (echo back formMessage with values)',
81
81
  'form={is_cancelled:true} - cancel a form',
82
82
  ],
83
83
  };
@@ -4,8 +4,7 @@
4
4
  * Deletes a persona with safety confirmation.
5
5
  */
6
6
  import { resolvePersona, normalizeTriggerType } from "../utils.js";
7
- import { createVersionStorage } from "../../../sync/version-storage.js";
8
- import { createVersionPolicyEngine } from "../../../sync/version-policy.js";
7
+ import { createCentralStorageEngine } from "../../../sync/central-factory.js";
9
8
  /**
10
9
  * Handle persona(mode="delete") - delete persona
11
10
  *
@@ -65,47 +64,27 @@ export async function handleDelete(args, client) {
65
64
  hint: "Use persona(method='delete', id='...', confirm=true) to proceed with deletion.",
66
65
  };
67
66
  }
68
- // Strict guidance: snapshot before any destructive change (local, forced)
67
+ // Best-effort: snapshot before any destructive change (via central DIS storage)
69
68
  const force = args.force;
70
69
  const targetEnv = args.env ?? "unknown";
71
70
  if (fullPersona) {
72
71
  try {
73
- const storage = createVersionStorage(process.cwd());
74
- const engine = createVersionPolicyEngine(storage);
75
- const snap = engine.forceCreateVersion(fullPersona, {
72
+ const { engine } = createCentralStorageEngine(client, fullPersona.id);
73
+ const snap = await engine.forceCreateVersion(fullPersona, {
76
74
  environment: targetEnv,
77
75
  tenant_id: targetEnv,
78
76
  message: "Before delete",
79
77
  created_by: "mcp-toolkit",
80
78
  });
81
- if (!snap.created || !snap.version) {
82
- if (!force) {
83
- return {
84
- error: "Failed to create pre-delete snapshot (required before delete)",
85
- persona_id: persona.id,
86
- details: snap.reason,
87
- hint: "Fix snapshotting (workspace storage) and retry deletion. Use force=true to bypass.",
88
- };
89
- }
90
- // force=true: proceed without snapshot (emergency only)
91
- }
92
- else {
79
+ if (snap.created && snap.version) {
93
80
  deletionSummary.version_snapshot = {
94
81
  id: snap.version.id,
95
82
  version_name: snap.version.version_name,
96
83
  };
97
84
  }
98
85
  }
99
- catch (e) {
100
- if (!force) {
101
- return {
102
- error: "Failed to create pre-delete snapshot (required before delete)",
103
- persona_id: persona.id,
104
- details: e instanceof Error ? e.message : String(e),
105
- hint: "Fix snapshotting (workspace storage) and retry deletion. Use force=true to bypass.",
106
- };
107
- }
108
- // force=true: proceed without snapshot (emergency only)
86
+ catch {
87
+ // Best-effort: snapshot failure does not block the delete
109
88
  }
110
89
  }
111
90
  // Perform deletion
@@ -16,8 +16,7 @@ import { PROJECT_TYPES } from "../../knowledge.js";
16
16
  import { resolvePersona, validateWidgetsForApi } from "../utils.js";
17
17
  import { validateSearchDataSourceConsistency, validationToHandlerResult } from "../workflow/validation.js";
18
18
  import { checkRemovedParams } from "../deprecation.js";
19
- import { createVersionStorage } from "../../../sync/version-storage.js";
20
- import { createVersionPolicyEngine } from "../../../sync/version-policy.js";
19
+ import { createCentralStorageEngine } from "../../../sync/central-factory.js";
21
20
  import { fingerprintPersona } from "../../../sync.js";
22
21
  /**
23
22
  * Apply WorkflowSpec changes to an existing workflow_def.
@@ -234,41 +233,23 @@ export async function handleUpdate(args, client) {
234
233
  hint: "Re-run persona(method='get') / workflow(mode='get') to fetch the latest state, re-apply your changes, then update again. Use force=true only if you intend to overwrite out-of-band changes.",
235
234
  };
236
235
  }
237
- // Strict guidance: snapshot before any update/change (local, forced)
236
+ // Best-effort: snapshot before any update/change (via central DIS storage)
238
237
  const targetEnv = args.env ?? "unknown";
239
238
  let versionSnapshot;
240
239
  try {
241
- const storage = createVersionStorage(process.cwd());
242
- const engine = createVersionPolicyEngine(storage);
243
- const snap = engine.forceCreateVersion(fullPersona, {
240
+ const { engine } = createCentralStorageEngine(client, fullPersona.id);
241
+ const snap = await engine.forceCreateVersion(fullPersona, {
244
242
  environment: targetEnv,
245
243
  tenant_id: targetEnv,
246
244
  message: "Pre-update snapshot",
247
245
  created_by: "mcp-toolkit",
248
246
  });
249
- if (!snap.created || !snap.version) {
250
- if (!force) {
251
- return {
252
- error: "Failed to create pre-update snapshot (required before update)",
253
- persona_id: persona.id,
254
- details: snap.reason,
255
- hint: "Fix snapshotting (workspace storage) or retry with force=true for emergency override.",
256
- };
257
- }
258
- }
259
- else {
247
+ if (snap.created && snap.version) {
260
248
  versionSnapshot = { id: snap.version.id, version_name: snap.version.version_name };
261
249
  }
262
250
  }
263
- catch (e) {
264
- if (!force) {
265
- return {
266
- error: "Failed to create pre-update snapshot (required before update)",
267
- persona_id: persona.id,
268
- details: e instanceof Error ? e.message : String(e),
269
- hint: "Retry after fixing local workspace write access, or use force=true for emergency override.",
270
- };
271
- }
251
+ catch {
252
+ // Best-effort: snapshot failure does not block the update
272
253
  }
273
254
  const existingProtoConfig = (fullPersona?.proto_config ?? persona.proto_config ?? {});
274
255
  const existingWorkflow = fullPersona
@@ -1,11 +1,11 @@
1
1
  /**
2
2
  * Version Management Handlers
3
3
  *
4
- * Provides local version tracking for personas (snapshot, history, restore).
5
- * Versions are stored in the local workspace, not on the Ema platform.
4
+ * Provides version tracking for personas (snapshot, history, restore).
5
+ * Uses IVersionStorage can be local (.ema-versions/) or central (DIS).
6
6
  */
7
- import { createVersionStorage } from "../../../sync/version-storage.js";
8
- import { createVersionPolicyEngine } from "../../../sync/version-policy.js";
7
+ import { createCentralStorageEngine, migrateLocalToCentral } from "../../../sync/central-factory.js";
8
+ import { VersionStorage } from "../../../sync/version-storage.js";
9
9
  /**
10
10
  * Check if a mode is a version management mode.
11
11
  */
@@ -32,20 +32,27 @@ export function isVersionMode(mode) {
32
32
  * @param versionContext - Workspace and environment context
33
33
  */
34
34
  export async function handleVersion(mode, args, client, persona, versionContext) {
35
- const storage = createVersionStorage(versionContext.workspaceRoot);
36
- const engine = createVersionPolicyEngine(storage);
35
+ const { storage, engine } = createCentralStorageEngine(client, persona.id);
37
36
  switch (mode) {
38
37
  // ─────────────────────────────────────────────────────────────────────────
39
38
  // snapshot / version_create - Create a new version snapshot
40
39
  // ─────────────────────────────────────────────────────────────────────────
41
40
  case "snapshot":
42
41
  case "version_create": {
42
+ // Lazy migration: seed central from local on first snapshot
43
+ try {
44
+ const localStorage = new VersionStorage(versionContext.workspaceRoot);
45
+ await migrateLocalToCentral(localStorage, storage, persona.id);
46
+ }
47
+ catch {
48
+ // Best-effort: migration failure should not block snapshot creation
49
+ }
43
50
  // Fetch full persona with workflow
44
51
  const fullPersona = await client.getPersonaById(persona.id);
45
52
  if (!fullPersona) {
46
53
  return { error: `Could not fetch full persona: ${persona.id}` };
47
54
  }
48
- const result = engine.forceCreateVersion(fullPersona, {
55
+ const result = await engine.forceCreateVersion(fullPersona, {
49
56
  environment: versionContext.environment,
50
57
  tenant_id: versionContext.tenant_id,
51
58
  message: args.message,
@@ -73,7 +80,15 @@ export async function handleVersion(mode, args, client, persona, versionContext)
73
80
  // ─────────────────────────────────────────────────────────────────────────
74
81
  case "history":
75
82
  case "version_list": {
76
- const versions = engine.listVersions(persona.id, {
83
+ // Lazy migration: seed central from local so history shows existing versions
84
+ try {
85
+ const localStorage = new VersionStorage(versionContext.workspaceRoot);
86
+ await migrateLocalToCentral(localStorage, storage, persona.id);
87
+ }
88
+ catch {
89
+ // Best-effort
90
+ }
91
+ const versions = await engine.listVersions(persona.id, {
77
92
  limit: args.limit,
78
93
  });
79
94
  return {
@@ -88,7 +103,7 @@ export async function handleVersion(mode, args, client, persona, versionContext)
88
103
  // ─────────────────────────────────────────────────────────────────────────
89
104
  case "version_get": {
90
105
  const versionId = args.version ?? "latest";
91
- const version = engine.getVersion(persona.id, versionId);
106
+ const version = await engine.getVersion(persona.id, versionId);
92
107
  if (!version) {
93
108
  return { error: `Version not found: ${versionId}` };
94
109
  }
@@ -126,7 +141,7 @@ export async function handleVersion(mode, args, client, persona, versionContext)
126
141
  if (!v1 || !v2) {
127
142
  return { error: "v1 and v2 required for version_compare mode" };
128
143
  }
129
- const result = engine.compareVersions(persona.id, v1, v2);
144
+ const result = await engine.compareVersions(persona.id, v1, v2);
130
145
  if (!result.success) {
131
146
  return { error: result.error };
132
147
  }
@@ -152,14 +167,14 @@ export async function handleVersion(mode, args, client, persona, versionContext)
152
167
  if (!versionId) {
153
168
  return { error: "version required for restore mode" };
154
169
  }
155
- const restoreData = engine.getRestoreData(persona.id, versionId);
170
+ const restoreData = await engine.getRestoreData(persona.id, versionId);
156
171
  if (!restoreData.success || !restoreData.restore_payload) {
157
172
  return { error: restoreData.error ?? "Failed to get restore data" };
158
173
  }
159
174
  // Create a version snapshot before restoring (audit trail)
160
175
  const fullPersona = await client.getPersonaById(persona.id);
161
176
  if (fullPersona) {
162
- engine.forceCreateVersion(fullPersona, {
177
+ await engine.forceCreateVersion(fullPersona, {
163
178
  environment: versionContext.environment,
164
179
  tenant_id: versionContext.tenant_id,
165
180
  message: `Before restore to ${restoreData.version?.version_name}`,
@@ -180,7 +195,7 @@ export async function handleVersion(mode, args, client, persona, versionContext)
180
195
  // Create post-restore version
181
196
  const restoredPersona = await client.getPersonaById(persona.id);
182
197
  if (restoredPersona) {
183
- engine.forceCreateVersion(restoredPersona, {
198
+ await engine.forceCreateVersion(restoredPersona, {
184
199
  environment: versionContext.environment,
185
200
  tenant_id: versionContext.tenant_id,
186
201
  message: `Restored to ${restoreData.version?.version_name}`,
@@ -207,7 +222,7 @@ export async function handleVersion(mode, args, client, persona, versionContext)
207
222
  args.auto_on_sync !== undefined ||
208
223
  args.max_versions !== undefined;
209
224
  if (hasUpdates) {
210
- const updated = engine.updatePolicy(persona.id, {
225
+ const updated = await engine.updatePolicy(persona.id, {
211
226
  auto_version_on_deploy: args.auto_on_deploy,
212
227
  auto_version_on_sync: args.auto_on_sync,
213
228
  max_versions: args.max_versions,
@@ -221,7 +236,7 @@ export async function handleVersion(mode, args, client, persona, versionContext)
221
236
  };
222
237
  }
223
238
  // Just get current policy
224
- const policy = engine.getPolicy(persona.id);
239
+ const policy = await engine.getPolicy(persona.id);
225
240
  return {
226
241
  persona_id: persona.id,
227
242
  persona_name: persona.name,
@@ -7,8 +7,7 @@
7
7
  * Extracted from server.ts to keep the dispatch table thin.
8
8
  */
9
9
  import { fingerprintPersona } from "../../../sync.js";
10
- import { createVersionStorage } from "../../../sync/version-storage.js";
11
- import { createVersionPolicyEngine } from "../../../sync/version-policy.js";
10
+ import { createCentralStorageEngine } from "../../../sync/central-factory.js";
12
11
  import { handleWorkflow } from "./index.js";
13
12
  import { handleWorkflowOptimize } from "./optimize.js";
14
13
  export async function handleWorkflowAdapter(args, createClient, getDefaultEnvName) {
@@ -84,60 +83,45 @@ export async function handleWorkflowAdapter(args, createClient, getDefaultEnvNam
84
83
  return { error: 'persona_id is required for workflow(mode="deploy")' };
85
84
  }
86
85
  const targetEnv = normalizedArgs.env ?? getDefaultEnvName();
86
+ // Fetch persona and validate fingerprint (blocking — these are safety checks)
87
+ const personaBefore = await client.getPersonaById(personaId);
88
+ if (!personaBefore) {
89
+ return { error: `Persona not found: ${personaId}` };
90
+ }
91
+ const currentFp = fingerprintPersona(personaBefore);
92
+ if (!force && !baseFingerprint) {
93
+ return {
94
+ error: "base_fingerprint is required for workflow deploy (stale-state protection)",
95
+ persona_id: personaId,
96
+ current_fingerprint: currentFp,
97
+ hint: "Run workflow(mode='get', persona_id='...') immediately before deploying and pass fingerprint as base_fingerprint. Use force=true only for emergency overrides.",
98
+ };
99
+ }
100
+ if (!force && baseFingerprint && baseFingerprint !== currentFp) {
101
+ return {
102
+ error: "Persona changed since you last fetched it (fingerprint mismatch)",
103
+ persona_id: personaId,
104
+ base_fingerprint: baseFingerprint,
105
+ current_fingerprint: currentFp,
106
+ hint: "Re-run workflow(mode='get') to fetch the latest workflow_def, re-apply your edits, then deploy again. Use force=true only if you intend to overwrite out-of-band changes.",
107
+ };
108
+ }
109
+ // Best-effort: snapshot before any destructive change (via central DIS storage)
87
110
  let versionCreated;
88
111
  try {
89
- const personaBefore = await client.getPersonaById(personaId);
90
- if (!personaBefore) {
91
- return { error: `Persona not found: ${personaId}` };
92
- }
93
- const currentFp = fingerprintPersona(personaBefore);
94
- if (!force && !baseFingerprint) {
95
- return {
96
- error: "base_fingerprint is required for workflow deploy (stale-state protection)",
97
- persona_id: personaId,
98
- current_fingerprint: currentFp,
99
- hint: "Run workflow(mode='get', persona_id='...') immediately before deploying and pass fingerprint as base_fingerprint. Use force=true only for emergency overrides.",
100
- };
101
- }
102
- if (!force && baseFingerprint && baseFingerprint !== currentFp) {
103
- return {
104
- error: "Persona changed since you last fetched it (fingerprint mismatch)",
105
- persona_id: personaId,
106
- base_fingerprint: baseFingerprint,
107
- current_fingerprint: currentFp,
108
- hint: "Re-run workflow(mode='get') to fetch the latest workflow_def, re-apply your edits, then deploy again. Use force=true only if you intend to overwrite out-of-band changes.",
109
- };
110
- }
111
- const storage = createVersionStorage(process.cwd());
112
- const engine = createVersionPolicyEngine(storage);
113
- const snap = engine.forceCreateVersion(personaBefore, {
112
+ const { engine } = createCentralStorageEngine(client, personaId);
113
+ const snap = await engine.forceCreateVersion(personaBefore, {
114
114
  environment: targetEnv,
115
115
  tenant_id: targetEnv,
116
116
  message: "Pre-deploy snapshot",
117
117
  created_by: "mcp-toolkit",
118
118
  });
119
- if (!snap.created || !snap.version) {
120
- if (!force) {
121
- return {
122
- error: "Failed to create pre-deploy snapshot (required before deploy)",
123
- persona_id: personaId,
124
- details: snap.reason,
125
- hint: "Fix snapshotting (workspace storage) or retry with force=true for emergency override.",
126
- };
127
- }
128
- }
129
- else {
119
+ if (snap.created && snap.version) {
130
120
  versionCreated = { id: snap.version.id, version_name: snap.version.version_name };
131
121
  }
132
122
  }
133
- catch (snapshotErr) {
134
- if (!force) {
135
- return {
136
- error: "Failed to create pre-deploy snapshot (required before deploy)",
137
- persona_id: personaId,
138
- hint: "Retry after fixing local workspace write access, or use force=true for emergency override.",
139
- };
140
- }
123
+ catch {
124
+ // Best-effort: snapshot failure does not block the deploy
141
125
  }
142
126
  const deployResult = await handleWorkflow({
143
127
  mode: "deploy",
@@ -222,7 +222,7 @@ export function getSearchNodeWidgetNames(workflowDef) {
222
222
  const elements = dc?.multiBinding?.elements ?? [];
223
223
  for (const el of elements) {
224
224
  const name = el.widgetConfig?.widgetName;
225
- if (typeof name === "string" && name.trim().length > 0) {
225
+ if (typeof name === "string" && name.trim().length > 0 && !name.startsWith("_")) {
226
226
  widgetNames.add(name.trim());
227
227
  }
228
228
  }
package/dist/mcp/tools.js CHANGED
@@ -53,7 +53,7 @@ export function generateTools(envNames, defaultEnv) {
53
53
  **IMPORTANT (strict)**:
54
54
  - Always fetch latest state immediately before changing: \`persona(method="get", id="abc", include_fingerprint=true)\`
55
55
  - Pass \`base_fingerprint\` to update to prevent overwriting out-of-band changes
56
- - The toolkit snapshots the persona locally before applying updates
56
+ - The toolkit automatically snapshots the persona before applying updates
57
57
 
58
58
  **For workflow changes**: Use the workflow tool:
59
59
  1. \`workflow(mode="get", persona_id="abc")\` - get current workflow_def
@@ -830,8 +830,35 @@ When the bot responds with a continuation (form, buttons, or text prompt), respo
830
830
 
831
831
  - **Text**: \`conversation(method="send", conversation_id="...", text="user reply")\`
832
832
  - **Buttons**: \`conversation(method="send", conversation_id="...", buttons={selected_button:{label:"Yes", description:"Confirm"}})\`
833
- - **Form submit**: \`conversation(method="send", conversation_id="...", form={fields:[{name:"email", value:{stringValue:"a@b.com"}}]})\`
834
833
  - **Form cancel**: \`conversation(method="send", conversation_id="...", form={is_cancelled:true})\`
834
+ - **Form submit**: Use \`raw_message\` — the simplified \`form\` param does NOT work for form submissions because the backend expects the full form structure echoed back.
835
+
836
+ ### Form HITL (CRITICAL — use raw_message)
837
+ Forms require echoing back the **entire formMessage** from the bot's response with your values filled in:
838
+
839
+ \`\`\`
840
+ conversation(method="send", conversation_id="...", original_message_id="<id from bot message>", raw_message={
841
+ type: "MESSAGE_TYPE_FORM",
842
+ formMessage: { ...entire formMessage from bot response with field values filled in... },
843
+ isUserMessage: true
844
+ })
845
+ \`\`\`
846
+
847
+ **Field value patterns by wellKnown type (from WellKnownValue proto):**
848
+ - **String** (\`WELL_KNOWN_TYPE_STRING\`): \`value: {wellKnown: {stringValue: "text"}}\`
849
+ - **Int** (\`WELL_KNOWN_TYPE_INT\`): \`value: {wellKnown: {int64Value: "42"}}\` (string-encoded integer)
850
+ - **Float** (\`WELL_KNOWN_TYPE_FLOAT\`): \`value: {wellKnown: {doubleValue: 3.14}}\`
851
+ - **Boolean** (\`WELL_KNOWN_TYPE_BOOL\`): \`value: {wellKnown: {boolValue: true}}\`
852
+ - **Date** (\`WELL_KNOWN_TYPE_DATE\`): \`value: {wellKnown: {dateValue: {year: 2026, month: 3, day: 2}}}\`
853
+ - **DateTime** (\`WELL_KNOWN_TYPE_DATETIME\`): \`value: {wellKnown: {datetimeValue: {utcTime: "2026-03-02T09:00:00Z"}}}\`
854
+
855
+ **Dropdown fields:** Check \`inputValidation.validValues\` to determine format:
856
+ - Struct options \`{wellKnown: {structValue: {Name:"Engineering", Id:"E"}}}\` → use the **Id** as stringValue: \`value: {wellKnown: {stringValue: "E"}}\`
857
+ - Enum options \`{enumValue: "Sick Leave"}\` → use the enum value as stringValue: \`value: {wellKnown: {stringValue: "Sick Leave"}}\`
858
+
859
+ **Intermediate forms**: If the response has \`isIntermediate: true\`, the bot will return another form with more fields after you submit. Previously filled fields will have \`editDisabled: true\`. The final form (no \`isIntermediate\`) submits all fields at once.
860
+
861
+ **Async responses**: Some workflows (e.g. agentic search) run long tasks after HITL exits. If \`send\` times out, poll with \`conversation(method="history")\` until the response appears with \`processing_status: "completed"\`.
835
862
 
836
863
  ## History
837
864
  - \`conversation(method="history", conversation_id="...")\` - get all messages in the conversation
@@ -850,7 +877,7 @@ When the bot responds with a continuation (form, buttons, or text prompt), respo
850
877
  - \`conversation(method="delete", conversation_id="...", confirm=true)\` - delete a conversation
851
878
 
852
879
  ## Raw Message
853
- - \`conversation(method="send", conversation_id="...", raw_message={type:"MESSAGE_TYPE_TEXT", ...})\` - send a pre-built ChatbotMessage dict`,
880
+ - \`conversation(method="send", conversation_id="...", raw_message={...})\` - send a pre-built ChatbotMessage dict. **Required for form HITL submissions.**`,
854
881
  inputSchema: {
855
882
  type: "object",
856
883
  properties: {
@@ -888,7 +915,7 @@ When the bot responds with a continuation (form, buttons, or text prompt), respo
888
915
  },
889
916
  form: {
890
917
  type: "object",
891
- description: "Form data for HITL continuation (for method=send). Include fields with values, or set is_cancelled=true to cancel.",
918
+ description: "Form HITL cancellation only (for method=send). Set is_cancelled=true to cancel a form. For form SUBMISSIONS, use raw_message instead the backend requires the full formMessage echoed back.",
892
919
  properties: {
893
920
  fields: {
894
921
  type: "array",
@@ -914,7 +941,7 @@ When the bot responds with a continuation (form, buttons, or text prompt), respo
914
941
  },
915
942
  raw_message: {
916
943
  type: "object",
917
- description: "Pre-built ChatbotMessage dict (for method=send). Use when convenience params (text/buttons/form) are insufficient.",
944
+ description: "Pre-built ChatbotMessage dict (for method=send). REQUIRED for form HITL submissions — echo back the full formMessage from the bot's response with values filled in. Use correct wellKnown value types: stringValue (strings), int64Value (ints, string-encoded), doubleValue (floats), boolValue (bools), dateValue {year,month,day} (dates), datetimeValue {utcTime} (datetimes). For struct dropdowns use the Id as stringValue; for enum dropdowns use the enumValue as stringValue.",
918
945
  },
919
946
  original_message_id: {
920
947
  type: "string",
@@ -345,8 +345,15 @@ export class EmaClientAdapter {
345
345
  /**
346
346
  * Delete a data source file
347
347
  */
348
- async deleteDataSource(personaId, fileId) {
349
- return this.client.deleteDataSource(personaId, fileId);
348
+ async deleteDataSource(personaId, fileId, opts) {
349
+ return this.client.deleteDataSource(personaId, fileId, opts);
350
+ }
351
+ /**
352
+ * Download a file's content by UploadedFile.Id.
353
+ * Uses GetSignedUrl gRPC → HTTP fetch.
354
+ */
355
+ async downloadFile(fileId, personaId) {
356
+ return this.client.downloadFile(fileId, personaId);
350
357
  }
351
358
  /**
352
359
  * List data source files
@@ -406,6 +406,50 @@ export class EmaClientV2 {
406
406
  async getDataSourceAggregates(personaId, widgetName) {
407
407
  return this.grpcClient.getContentNodeAggregates(personaId, widgetName);
408
408
  }
409
+ /**
410
+ * Download a file's content by its UploadedFile.Id.
411
+ * Uses GetSignedUrl gRPC → HTTP fetch on the signed URL.
412
+ *
413
+ * @param fileId - The UploadedFile.Id (from ContentNodeResponse.id)
414
+ * @returns Raw file content as Buffer
415
+ * @throws If file is deleted or signed URL fetch fails
416
+ */
417
+ async downloadFile(fileId, personaId) {
418
+ const signedUrlResponse = await this.grpcClient.getSignedUrl(fileId, personaId);
419
+ if (signedUrlResponse.isDeleted) {
420
+ throw new EmaApiError({
421
+ statusCode: 410,
422
+ body: `File ${fileId} has been deleted`,
423
+ message: `downloadFile: file is deleted (${this.env.name})`,
424
+ });
425
+ }
426
+ if (!signedUrlResponse.signedUrl) {
427
+ throw new EmaApiError({
428
+ statusCode: 404,
429
+ body: `No signed URL returned for file ${fileId}`,
430
+ message: `downloadFile: no signed URL (${this.env.name})`,
431
+ });
432
+ }
433
+ const controller = new AbortController();
434
+ const timeoutId = setTimeout(() => controller.abort(), 30_000);
435
+ try {
436
+ const resp = await fetch(signedUrlResponse.signedUrl, {
437
+ signal: controller.signal,
438
+ });
439
+ if (!resp.ok) {
440
+ throw new EmaApiError({
441
+ statusCode: resp.status,
442
+ body: await resp.text(),
443
+ message: `downloadFile: fetch failed (${this.env.name})`,
444
+ });
445
+ }
446
+ const arrayBuffer = await resp.arrayBuffer();
447
+ return Buffer.from(arrayBuffer);
448
+ }
449
+ finally {
450
+ clearTimeout(timeoutId);
451
+ }
452
+ }
409
453
  /**
410
454
  * Replicate data between personas
411
455
  */
@@ -720,34 +764,16 @@ export class EmaClientV2 {
720
764
  }
721
765
  }
722
766
  /**
723
- * Delete a data source file from an AI Employee's knowledge base.
724
- * API endpoint: DELETE /api/v2/upload/files/{file_id}
767
+ * Delete data source file(s) from an AI Employee's knowledge base.
768
+ * Uses gRPC DeleteFilesForGroup — the ONLY deletion method DIS exposes.
769
+ *
770
+ * @param personaId - The persona owning the files
771
+ * @param fileId - Single file ID to delete
772
+ * @param opts - Optional widget name scope
725
773
  */
726
- async deleteDataSource(personaId, fileId) {
727
- const controller = new AbortController();
728
- const timeoutId = setTimeout(() => controller.abort(), 30_000);
729
- try {
730
- const resp = await fetch(`${this.env.baseUrl.replace(/\/$/, "")}/api/v2/upload/files/${fileId}?persona_id=${personaId}`, {
731
- method: "DELETE",
732
- headers: {
733
- Authorization: `Bearer ${this.env.bearerToken}`,
734
- "X-Persona-Id": personaId,
735
- },
736
- signal: controller.signal,
737
- });
738
- if (!resp.ok && resp.status !== 404) {
739
- const errorBody = await resp.text();
740
- throw new EmaApiError({
741
- statusCode: resp.status,
742
- body: errorBody,
743
- message: `deleteDataSource failed (${this.env.name})`,
744
- });
745
- }
746
- return { success: true, fileId };
747
- }
748
- finally {
749
- clearTimeout(timeoutId);
750
- }
774
+ async deleteDataSource(personaId, fileId, opts) {
775
+ await this.grpcClient.deleteFilesForGroup(personaId, [fileId], opts?.widgetName);
776
+ return { success: true, fileId };
751
777
  }
752
778
  /**
753
779
  * Delete a persona