@hailer/mcp 0.1.6 → 0.1.9

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 (137) hide show
  1. package/.claude/agents/agent-dmitri-activity-crud.md +3 -1
  2. package/.claude/agents/agent-giuseppe-app-builder.md +11 -12
  3. package/.claude/agents/agent-kenji-data-reader.md +5 -3
  4. package/.claude/hooks/sync-marketplace-agents.cjs +117 -56
  5. package/.claude/skills/hailer-app-builder/SKILL.md +506 -0
  6. package/.claude/skills/publish-hailer-app/SKILL.md +169 -0
  7. package/.claude/skills/tool-parameter-usage/SKILL.md +112 -0
  8. package/CHANGELOG.md +20 -0
  9. package/CLAUDE.md +37 -16
  10. package/REFACTOR_STATUS.md +127 -0
  11. package/dist/cli.js +0 -0
  12. package/dist/client/agents/base.d.ts +202 -0
  13. package/dist/client/agents/base.js +737 -0
  14. package/dist/client/agents/definitions.d.ts +53 -0
  15. package/dist/client/agents/definitions.js +178 -0
  16. package/dist/client/agents/orchestrator.d.ts +119 -0
  17. package/dist/client/agents/orchestrator.js +760 -0
  18. package/dist/client/agents/specialist.d.ts +86 -0
  19. package/dist/client/agents/specialist.js +340 -0
  20. package/dist/client/bot-manager.d.ts +44 -0
  21. package/dist/client/bot-manager.js +173 -0
  22. package/dist/client/chat-agent-daemon.d.ts +464 -0
  23. package/dist/client/chat-agent-daemon.js +1774 -0
  24. package/dist/client/daemon-factory.d.ts +106 -0
  25. package/dist/client/daemon-factory.js +301 -0
  26. package/dist/client/factory.d.ts +107 -0
  27. package/dist/client/factory.js +304 -0
  28. package/dist/client/index.d.ts +17 -0
  29. package/dist/client/index.js +38 -0
  30. package/dist/client/multi-bot-manager.d.ts +18 -0
  31. package/dist/client/multi-bot-manager.js +88 -1
  32. package/dist/client/orchestrator-daemon.d.ts +87 -0
  33. package/dist/client/orchestrator-daemon.js +444 -0
  34. package/dist/client/services/agent-registry.d.ts +108 -0
  35. package/dist/client/services/agent-registry.js +630 -0
  36. package/dist/client/services/conversation-manager.d.ts +50 -0
  37. package/dist/client/services/conversation-manager.js +136 -0
  38. package/dist/client/services/mcp-client.d.ts +48 -0
  39. package/dist/client/services/mcp-client.js +105 -0
  40. package/dist/client/services/message-classifier.d.ts +37 -0
  41. package/dist/client/services/message-classifier.js +187 -0
  42. package/dist/client/services/message-formatter.d.ts +84 -0
  43. package/dist/client/services/message-formatter.js +353 -0
  44. package/dist/client/services/session-logger.d.ts +106 -0
  45. package/dist/client/services/session-logger.js +446 -0
  46. package/dist/client/services/tool-executor.d.ts +41 -0
  47. package/dist/client/services/tool-executor.js +169 -0
  48. package/dist/client/services/workspace-schema-cache.d.ts +149 -0
  49. package/dist/client/services/workspace-schema-cache.js +732 -0
  50. package/dist/client/specialist-daemon.d.ts +77 -0
  51. package/dist/client/specialist-daemon.js +197 -0
  52. package/dist/client/specialists.d.ts +53 -0
  53. package/dist/client/specialists.js +178 -0
  54. package/dist/client/tool-schema-loader.d.ts +4 -3
  55. package/dist/client/tool-schema-loader.js +54 -8
  56. package/dist/client/types.d.ts +283 -55
  57. package/dist/client/types.js +113 -2
  58. package/dist/config.d.ts +1 -1
  59. package/dist/config.js +1 -1
  60. package/dist/core.d.ts +10 -2
  61. package/dist/core.js +43 -27
  62. package/dist/lib/logger.js +15 -3
  63. package/dist/mcp/UserContextCache.js +2 -2
  64. package/dist/mcp/hailer-clients.js +5 -5
  65. package/dist/mcp/signal-handler.js +27 -5
  66. package/dist/mcp/tools/activity.js +137 -65
  67. package/dist/mcp/tools/app-core.js +4 -140
  68. package/dist/mcp/tools/app-marketplace.js +15 -260
  69. package/dist/mcp/tools/app-member.js +2 -73
  70. package/dist/mcp/tools/app-scaffold.js +146 -87
  71. package/dist/mcp/tools/discussion.js +348 -73
  72. package/dist/mcp/tools/insight.js +74 -190
  73. package/dist/mcp/tools/workflow.js +20 -94
  74. package/dist/mcp/utils/hailer-api-client.d.ts +4 -2
  75. package/dist/mcp/utils/hailer-api-client.js +24 -10
  76. package/dist/mcp-server.d.ts +4 -0
  77. package/dist/mcp-server.js +24 -4
  78. package/dist/routes/agents.d.ts +44 -0
  79. package/dist/routes/agents.js +311 -0
  80. package/dist/services/agent-credential-store.d.ts +73 -0
  81. package/dist/services/agent-credential-store.js +212 -0
  82. package/lineup-manager/dist/assets/index-8ce6041d.css +1 -0
  83. package/lineup-manager/dist/assets/index-e168f265.js +600 -0
  84. package/lineup-manager/dist/index.html +15 -0
  85. package/lineup-manager/dist/manifest.json +17 -0
  86. package/lineup-manager/dist/vite.svg +1 -0
  87. package/package.json +1 -1
  88. package/dist/client/adaptive-documentation-bot.d.ts +0 -106
  89. package/dist/client/adaptive-documentation-bot.js +0 -464
  90. package/dist/client/adaptive-documentation-types.d.ts +0 -66
  91. package/dist/client/adaptive-documentation-types.js +0 -9
  92. package/dist/client/agent-activity-bot.d.ts +0 -51
  93. package/dist/client/agent-activity-bot.js +0 -166
  94. package/dist/client/agent-tracker.d.ts +0 -499
  95. package/dist/client/agent-tracker.js +0 -659
  96. package/dist/client/description-updater.d.ts +0 -56
  97. package/dist/client/description-updater.js +0 -259
  98. package/dist/client/log-parser.d.ts +0 -72
  99. package/dist/client/log-parser.js +0 -387
  100. package/dist/client/mcp-assistant.d.ts +0 -21
  101. package/dist/client/mcp-assistant.js +0 -58
  102. package/dist/client/mcp-client.d.ts +0 -50
  103. package/dist/client/mcp-client.js +0 -538
  104. package/dist/client/message-processor.d.ts +0 -35
  105. package/dist/client/message-processor.js +0 -357
  106. package/dist/client/providers/anthropic-provider.d.ts +0 -19
  107. package/dist/client/providers/anthropic-provider.js +0 -645
  108. package/dist/client/providers/assistant-provider.d.ts +0 -17
  109. package/dist/client/providers/assistant-provider.js +0 -51
  110. package/dist/client/providers/llm-provider.d.ts +0 -47
  111. package/dist/client/providers/llm-provider.js +0 -367
  112. package/dist/client/providers/openai-provider.d.ts +0 -23
  113. package/dist/client/providers/openai-provider.js +0 -630
  114. package/dist/client/simple-llm-caller.d.ts +0 -19
  115. package/dist/client/simple-llm-caller.js +0 -100
  116. package/dist/client/skill-generator.d.ts +0 -81
  117. package/dist/client/skill-generator.js +0 -386
  118. package/dist/client/test-adaptive-bot.d.ts +0 -9
  119. package/dist/client/test-adaptive-bot.js +0 -82
  120. package/dist/client/token-pricing.d.ts +0 -38
  121. package/dist/client/token-pricing.js +0 -127
  122. package/dist/client/token-tracker.d.ts +0 -232
  123. package/dist/client/token-tracker.js +0 -457
  124. package/dist/client/token-usage-bot.d.ts +0 -53
  125. package/dist/client/token-usage-bot.js +0 -153
  126. package/dist/client/tool-executor.d.ts +0 -69
  127. package/dist/client/tool-executor.js +0 -159
  128. package/dist/lib/materialize.d.ts +0 -3
  129. package/dist/lib/materialize.js +0 -101
  130. package/dist/lib/normalizedName.d.ts +0 -7
  131. package/dist/lib/normalizedName.js +0 -48
  132. package/dist/lib/terminal-prompt.d.ts +0 -9
  133. package/dist/lib/terminal-prompt.js +0 -108
  134. package/dist/mcp/tools/skill.d.ts +0 -10
  135. package/dist/mcp/tools/skill.js +0 -279
  136. package/dist/mcp/tools/workflow-template.d.ts +0 -19
  137. package/dist/mcp/tools/workflow-template.js +0 -822
@@ -95,7 +95,7 @@ class UserContextCache {
95
95
  * Separated from getContext to support Promise memoization.
96
96
  */
97
97
  static async createContextInternal(apiKey) {
98
- logger.info('Creating new user context', { apiKey: apiKey.substring(0, 8) + '...' });
98
+ logger.debug('Creating new user context', { apiKey: apiKey.substring(0, 8) + '...' });
99
99
  try {
100
100
  // Create client connection (uses existing connection pool)
101
101
  const client = await (0, hailer_clients_1.createHailerClientByApiKey)(apiKey);
@@ -124,7 +124,7 @@ class UserContextCache {
124
124
  const rawInitSize = Buffer.byteLength(JSON.stringify(init), 'utf8');
125
125
  const workspaceCacheSize = Buffer.byteLength(JSON.stringify(workspaceCache), 'utf8');
126
126
  const totalContextSize = Buffer.byteLength(JSON.stringify({ init, workspaceCache }), 'utf8');
127
- logger.info('User context created and cached', {
127
+ logger.debug('User context created and cached', {
128
128
  apiKey: apiKey.substring(0, 8) + '...',
129
129
  workflowCount: init.processes?.length || 0,
130
130
  userCount: workspaceCache.users.length,
@@ -20,7 +20,7 @@ async function getCurrentUserId(client) {
20
20
  ]));
21
21
  // Check if there's a user field (this is the current authenticated user)
22
22
  if (init.user && init.user._id) {
23
- logger.info('Retrieved user ID from init', { userId: init.user._id });
23
+ logger.debug('Retrieved user ID from init', { userId: init.user._id });
24
24
  return init.user._id;
25
25
  }
26
26
  // Fallback: Try with different parameters if the above doesn't work
@@ -33,7 +33,7 @@ async function getCurrentUserId(client) {
33
33
  if (profileResponse.ok) {
34
34
  const profile = (await profileResponse.json());
35
35
  if (profile && profile._id) {
36
- logger.info('Retrieved user ID from profile endpoint', { userId: profile._id });
36
+ logger.debug('Retrieved user ID from profile endpoint', { userId: profile._id });
37
37
  return profile._id;
38
38
  }
39
39
  }
@@ -52,7 +52,7 @@ async function getCurrentUserId(client) {
52
52
  const member = members[0];
53
53
  const userId = member.uid || member._id || member.id;
54
54
  if (userId) {
55
- logger.info('Inferred user ID from single workspace member', { userId });
55
+ logger.debug('Inferred user ID from single workspace member', { userId });
56
56
  return userId;
57
57
  }
58
58
  }
@@ -205,7 +205,7 @@ const createHailerClientByApiKey = async (apiKey) => {
205
205
  // Check if we have an existing connection
206
206
  let clientManager = connectionPool.get(connectionKey);
207
207
  if (clientManager && clientManager.isConnected()) {
208
- logger.info('Reusing existing connection', {
208
+ logger.debug('Reusing existing connection', {
209
209
  apiKey: apiKey.substring(0, 8) + '...',
210
210
  email: account.email
211
211
  });
@@ -213,7 +213,7 @@ const createHailerClientByApiKey = async (apiKey) => {
213
213
  }
214
214
  // Clear any stale connection
215
215
  if (clientManager) {
216
- logger.info('Cleaning up stale connection', {
216
+ logger.debug('Cleaning up stale connection', {
217
217
  apiKey: apiKey.substring(0, 8) + '...',
218
218
  email: account.email
219
219
  });
@@ -24,11 +24,34 @@ class SignalHandler {
24
24
  handleIncomingSignal(rawSignal) {
25
25
  try {
26
26
  const [eventType, eventData] = rawSignal;
27
+ // Extract workspaceId from signal data (sid) - required for multi-workspace isolation
28
+ // Falls back to currentWorkspace only if signal doesn't include workspace info
29
+ const signalWorkspaceId = eventData.sid;
30
+ const workspaceId = signalWorkspaceId || this.workspaceCache?.currentWorkspace._id;
31
+ // Log if workspace mismatch detected (signal from different workspace)
32
+ if (signalWorkspaceId && this.workspaceCache?.currentWorkspace._id &&
33
+ signalWorkspaceId !== this.workspaceCache.currentWorkspace._id) {
34
+ logger.debug('Signal from different workspace', {
35
+ signalType: eventType,
36
+ signalWorkspaceId,
37
+ currentWorkspaceId: this.workspaceCache.currentWorkspace._id,
38
+ });
39
+ }
40
+ // Debug: log raw signal data to discover available fields
41
+ if (eventType === 'messenger.new') {
42
+ const data = eventData;
43
+ logger.debug('Raw messenger.new signal data', {
44
+ eventType,
45
+ dataKeys: Object.keys(eventData),
46
+ sid: String(data.sid || ''),
47
+ workspaceId: String(data.workspaceId || ''),
48
+ });
49
+ }
27
50
  const signal = {
28
51
  type: eventType,
29
52
  data: eventData,
30
53
  timestamp: Date.now(),
31
- workspaceId: this.workspaceCache?.currentWorkspace._id,
54
+ workspaceId,
32
55
  };
33
56
  // Add to history
34
57
  this.addToHistory(signal);
@@ -134,14 +157,13 @@ class SignalHandler {
134
157
  logger.debug('No workspace cache available, skipping cache invalidation');
135
158
  return;
136
159
  }
137
- const meta = signal.data || {};
138
160
  logger.debug('Cache invalidate signal received', {
139
161
  workspaceId: signal.workspaceId,
140
- meta
162
+ meta: signal.data
141
163
  });
142
164
  try {
143
- // Refresh cache by fetching fresh data - pass meta to let backend decide what to return
144
- const init = await this.client.socket.request('v2.core.init', [meta]);
165
+ // Refresh cache by fetching fresh data - API requires array of keys to fetch
166
+ const init = await this.client.socket.request('v2.core.init', [['users', 'network', 'networks']]);
145
167
  // Update workspace cache with fresh data
146
168
  if (init.processes) {
147
169
  this.workspaceCache.rawInit.processes = init.processes;
@@ -318,21 +318,74 @@ function buildActivityUpdate(args, context) {
318
318
  throw new Error(`Invalid fields JSON: ${args.fields}`);
319
319
  }
320
320
  }
321
- if (parsedFields &&
322
- typeof parsedFields === "object" &&
323
- context.workspaceCache) {
321
+ // Auto-fix activitylink fields: if LLM passes an object instead of just the ID string
322
+ if (parsedFields && typeof parsedFields === "object") {
323
+ for (const [fieldId, fieldValue] of Object.entries(parsedFields)) {
324
+ if (fieldValue && typeof fieldValue === "object" && !Array.isArray(fieldValue)) {
325
+ const obj = fieldValue;
326
+ let extractedId = null;
327
+ // Case 1: {type: "activitylink", value: {_id: "...", name: "..."}}
328
+ if (obj.type && obj.value && typeof obj.value === "object" && obj.value._id) {
329
+ extractedId = obj.value._id;
330
+ }
331
+ // Case 2: {_id: "...", name: "..."}
332
+ else if (obj._id && typeof obj._id === "string") {
333
+ extractedId = obj._id;
334
+ }
335
+ // Case 3: {value: "..."} where value is the ID string
336
+ else if (obj.value && typeof obj.value === "string" && /^[a-f0-9]{24}$/i.test(obj.value)) {
337
+ extractedId = obj.value;
338
+ }
339
+ if (extractedId) {
340
+ logger.warn("Auto-fixing activitylink field - extracting ID from object", {
341
+ fieldId,
342
+ original: JSON.stringify(fieldValue),
343
+ extracted: extractedId,
344
+ });
345
+ parsedFields[fieldId] = extractedId;
346
+ }
347
+ }
348
+ }
349
+ }
350
+ // Field-type aware validation: only validate user fields as users
351
+ if (parsedFields && typeof parsedFields === "object" && context.workspaceCache) {
324
352
  const invalidUsers = [];
325
353
  for (const [fieldId, fieldValue] of Object.entries(parsedFields)) {
326
- if (typeof fieldValue === "string" &&
327
- /^[a-f0-9]{24}$/i.test(fieldValue)) {
354
+ // Only check string values that look like IDs
355
+ if (typeof fieldValue !== "string" || !/^[a-f0-9]{24}$/i.test(fieldValue)) {
356
+ continue;
357
+ }
358
+ // Find the field definition to check its type
359
+ let fieldType;
360
+ for (const workflow of context.workspaceCache.rawInit.processes) {
361
+ const workflowFields = workflow.fields;
362
+ if (!workflowFields)
363
+ continue;
364
+ // Handle both array and object formats for fields
365
+ let field;
366
+ if (Array.isArray(workflowFields)) {
367
+ field = workflowFields.find((f) => f._id === fieldId || f.id === fieldId);
368
+ }
369
+ else if (typeof workflowFields === 'object') {
370
+ // Fields as object: { fieldId: fieldData, ... }
371
+ field = workflowFields[fieldId];
372
+ }
373
+ if (field) {
374
+ fieldType = field.type;
375
+ break;
376
+ }
377
+ }
378
+ // Only validate as user ID if the field type is 'user'
379
+ if (fieldType === 'user') {
328
380
  const user = (0, workspace_cache_1.getUserById)(context.workspaceCache, fieldValue);
329
381
  if (!user) {
330
382
  invalidUsers.push(`${fieldId}: ${fieldValue}`);
331
383
  }
332
384
  }
385
+ // Skip validation for 'activitylink' and other field types
333
386
  }
334
387
  if (invalidUsers.length > 0) {
335
- throw new Error(`Invalid user IDs in fields: ${invalidUsers.join(", ")}. Use search_workspace_users to find valid user IDs.`);
388
+ throw new Error(`Invalid user IDs in user fields: ${invalidUsers.join(", ")}. Use search_workspace_users to find valid user IDs.`);
336
389
  }
337
390
  }
338
391
  return {
@@ -369,7 +422,7 @@ function formatUpdateActivityResponse(args, result) {
369
422
  exports.listActivitiesTool = {
370
423
  name: 'list_activities',
371
424
  group: tool_registry_1.ToolGroup.READ,
372
- description: `📋 Retrieves list of activities for a specific workflow phase. ⚠️ CRITICAL: phaseId is MANDATORY string parameter, must match workflow's current phase. ✅ CORRECT: {"workflowId":"6756e099410306cab03a7dfb","phaseId":"specific_phase_identifier"} ❌ WRONG: {"workflowId":"6756e099410306cab03a7dfb"} (missing required phaseId)`,
425
+ description: `List activities from workflow phase`,
373
426
  schema: zod_1.z.object({
374
427
  workflowId: zod_1.z.string().describe("Workflow ID to list activities from"),
375
428
  phaseId: zod_1.z.string().describe("Phase ID to filter activities (required - use list_workflow_phases to get available phases)"),
@@ -399,7 +452,7 @@ exports.listActivitiesTool = {
399
452
  value: zod_1.z.union([zod_1.z.string(), zod_1.z.number()]).optional().describe("Value to filter by - REQUIRED for text_search, equals, not_equals, contains, greater_than, less_than operators. NOT USED for range operator (use start/end instead). For text_search: use the text/name to search for. For equals: use the entity ID."),
400
453
  start: zod_1.z.coerce.number().optional().describe("Start value - ONLY for range operator (NOT in 'value' field). Unix timestamp in milliseconds for date ranges. Example: for June 2025 date range, use start=1717200000000 directly here, NOT nested in 'value'."),
401
454
  end: zod_1.z.coerce.number().optional().describe("End value - ONLY for range operator (NOT in 'value' field). Unix timestamp in milliseconds for date ranges. Example: for June 2025 date range, use end=1719791999000 directly here, NOT nested in 'value'."),
402
- })).optional().describe("Field-based filters - PREFERRED over search parameter for reliable results. Key is the field ID from get_workflow_schema. CRITICAL: For range operator, use 'start' and 'end' as direct properties, NOT nested inside 'value'. Example: {fieldId: {operator: 'range', start: 1717200000000, end: 1719791999000}}. BEST use cases: 1) Date ranges: use range operator with created/updated field IDs, 2) Company/site by NAME: use text_search operator, 3) Exact ID matches: use equals operator."),
455
+ })).optional().describe("Filter by field values. STRUCTURE: {\"fieldId\": {\"operator\": \"...\", \"value\": \"...\"}}. EXAMPLE: To find player named 'Harry Kane', use: {\"691ffdf84217e9e8434e5694\": {\"operator\": \"text_search\", \"value\": \"Harry Kane\"}}. WRONG: {\"playerName\": \"Harry Kane\"} or {\"fieldId\": \"Harry Kane\"}. Operators: text_search (partial text match), equals (exact ID match), range (use start/end instead of value)."),
403
456
  search: zod_1.z.string().optional().describe("Text search across activity content - USE FILTERS INSTEAD when possible for reliable results. Only use search for truly exploratory queries."),
404
457
  limit: zod_1.z.coerce.number().optional().default(50).describe("Maximum number of activities to return (default: 50, max: 20000)"),
405
458
  page: zod_1.z.coerce.number().optional().default(0).describe("Page number for pagination (0-based)"),
@@ -500,7 +553,7 @@ exports.listActivitiesTool = {
500
553
  exports.showActivityByIdTool = {
501
554
  name: 'show_activity_by_id',
502
555
  group: tool_registry_1.ToolGroup.READ,
503
- description: `📄 Load specific activity by ID with full details`,
556
+ description: `Get activity by ID`,
504
557
  schema: zod_1.z.object({
505
558
  activityId: zod_1.z.string().describe("Activity ID to load"),
506
559
  }),
@@ -535,33 +588,7 @@ exports.showActivityByIdTool = {
535
588
  // ============================================================================
536
589
  // TOOL 3: CREATE ACTIVITY
537
590
  // ============================================================================
538
- const createActivityDescription = `Create one or multiple activities - Supports both single and bulk creation
539
-
540
- **BULK CREATION (3+ activities):**
541
- Use the 'activities' parameter to create multiple activities efficiently:
542
- - activities: [{ name, fields, phaseId }, { name, fields, phaseId }, ...]
543
- - All activities created in one API call
544
- - Much faster for creating 10, 50, or 100+ activities
545
- - Recommended for sample data generation and batch imports
546
-
547
- **SINGLE CREATION:**
548
- Use individual parameters (name, fields, phaseId) for creating one activity
549
-
550
- **IMPORTANT - Field Type Rules:**
551
- - textpredefinedoptions: MUST be a single STRING value (e.g., "Option1"), NOT an array
552
- - activitylink: MUST be a STRING (activity ID), NOT an array
553
- - users/teams: STRING user/team ID
554
- - numeric/date: NUMBER (timestamp for dates)
555
- - text/textarea: STRING
556
-
557
- **Field Keys vs Field IDs:**
558
- If workflow uses field 'key' (recommended), you can use readable names:
559
- - With keys: { "vendor": "activity-id", "priority": "High" }
560
- - Without keys: { "507f191e810c19729de860ea": "activity-id" }
561
-
562
- **Examples:**
563
- Single: { workflowId, name: "Customer Lead", fields: { customerName: "John" } }
564
- Bulk: { workflowId, activities: [{ name: "Lead 1", fields: {...} }, { name: "Lead 2", fields: {...} }] }`;
591
+ const createActivityDescription = `Create activity (single or bulk via activities array)`;
565
592
  exports.createActivityTool = {
566
593
  name: 'create_activity',
567
594
  group: tool_registry_1.ToolGroup.WRITE,
@@ -835,31 +862,7 @@ ${createdActivities.slice(0, 10).map((act, idx) => `${idx + 1}. "${act.name}" (I
835
862
  // ============================================================================
836
863
  // TOOL 4: UPDATE ACTIVITY
837
864
  // ============================================================================
838
- const updateActivityDescription = `Update one or multiple activities in any Hailer workflow
839
-
840
- **BULK UPDATE (3+ activities):**
841
- Use the 'activities' parameter to update multiple activities efficiently:
842
- - activities: [{ _id, name?, fields? }, { _id, name?, fields? }, ...]
843
- - All activities updated in one API call
844
- - Much faster for updating 10, 50, or 100+ activities
845
- - Recommended for batch operations and data migrations
846
-
847
- **SINGLE UPDATE:**
848
- Use individual parameters (_id or activityId, name, fields, phaseId) for updating one activity
849
-
850
- **IMPORTANT - ActivityLink Field Values:**
851
- When updating activitylink fields, use STRING values (the activity ID to link to), NOT arrays:
852
- - ✅ Correct: { "vendorField": "507f1f77bcf86cd799439011" }
853
- - ❌ Wrong: { "vendorField": ["507f1f77bcf86cd799439011"] }
854
-
855
- **Field Keys vs Field IDs:**
856
- If workflow uses field 'key' (recommended), you can use readable names:
857
- - With keys: { "vendor": "new-activity-id", "priority": "High" }
858
- - Without keys: { "507f191e810c19729de860ea": "new-activity-id" }
859
-
860
- **Examples:**
861
- Single: update_activity({ activityId: "xxx", fields: { status: "Active" } })
862
- Bulk: update_activity({ activities: [{ _id: "xxx", fields: { status: "Active" } }, { _id: "yyy", fields: { status: "Inactive" } }] })`;
865
+ const updateActivityDescription = `Update activity (single or bulk via activities array)`;
863
866
  exports.updateActivityTool = {
864
867
  name: 'update_activity',
865
868
  group: tool_registry_1.ToolGroup.WRITE,
@@ -876,7 +879,7 @@ exports.updateActivityTool = {
876
879
  }))
877
880
  .min(1))
878
881
  .optional()
879
- .describe("BULK: Array of activities to update. Use this for updating 3+ activities efficiently. If provided, single activity parameters (activityId, name, fields) are ignored."),
882
+ .describe("BULK: Array of activities to update. Each object MUST have: '_id' (NOT 'activityId'), and optionally 'fields' (NOT 'fieldsAndValues'). ❌ WRONG: {activityId: 'x', fieldsAndValues: {...}} ✅ CORRECT: {_id: 'x', fields: {...}}"),
880
883
  // SINGLE: Individual activity parameters
881
884
  activityId: zod_1.z
882
885
  .string()
@@ -887,7 +890,7 @@ exports.updateActivityTool = {
887
890
  fields: zod_1.z
888
891
  .union([zod_1.z.record(zod_1.z.any()), zod_1.z.string()])
889
892
  .optional()
890
- .describe("SINGLE: Updated field values as key-value pairs. IMPORTANT: activitylink values must be STRINGS (activity IDs), not arrays. Example: { 'vendor': 'activity-id-string', 'priority': 'High' }. Can be object or JSON string"),
893
+ .describe("SINGLE: Updated field values as key-value pairs. CRITICAL for activitylink fields: pass ONLY the activity ID as a plain STRING, NOT an object! ❌ WRONG: {'fieldId': {'_id': 'abc', 'name': 'X'}} ✅ CORRECT: {'fieldId': 'abc123...'}. Example: { '691fff...': '691ffe...' } for linking to another activity. Can be object or JSON string."),
891
894
  phaseId: zod_1.z
892
895
  .string()
893
896
  .optional()
@@ -897,7 +900,35 @@ exports.updateActivityTool = {
897
900
  try {
898
901
  // BULK MODE: Update multiple activities
899
902
  if (args.activities && Array.isArray(args.activities)) {
900
- logger.debug("Bulk update mode", { activityCount: args.activities.length });
903
+ logger.debug("Bulk update mode", {
904
+ activityCount: args.activities.length,
905
+ activities: JSON.stringify(args.activities, null, 2)
906
+ });
907
+ // Auto-fix common parameter name mistakes
908
+ for (const activity of args.activities) {
909
+ // Fix activityId -> _id
910
+ if (!activity._id && activity.activityId) {
911
+ logger.warn("Auto-fixing: activityId -> _id", { activityId: activity.activityId });
912
+ activity._id = activity.activityId;
913
+ delete activity.activityId;
914
+ }
915
+ // Fix fieldsAndValues -> fields
916
+ if (!activity.fields && activity.fieldsAndValues) {
917
+ logger.warn("Auto-fixing: fieldsAndValues -> fields", { activityId: activity._id });
918
+ activity.fields = activity.fieldsAndValues;
919
+ delete activity.fieldsAndValues;
920
+ }
921
+ }
922
+ // Validate that all activities have _id
923
+ const invalidActivities = args.activities.filter((a) => !a._id || typeof a._id !== 'string' || a._id.length < 24);
924
+ if (invalidActivities.length > 0) {
925
+ return {
926
+ content: [{
927
+ type: "text",
928
+ text: `❌ **Error: Invalid bulk update request**\n\nEach activity in the "activities" array MUST have a valid "_id" field (24-character activity ID).\n\n**What you sent:** ${JSON.stringify(args.activities, null, 2)}\n\n**Correct format:**\n\`\`\`json\n{\n "activities": [\n { "_id": "691ffe654217e9e8434e577c", "fields": { "fieldId": "value" } },\n { "_id": "691ffe654217e9e8434e5774", "name": "New Name" }\n ]\n}\n\`\`\`\n\n**Tip:** First use list_activities to get the activity IDs, then pass them in the _id field.`,
929
+ }],
930
+ };
931
+ }
901
932
  // Build updates for each activity
902
933
  const updates = args.activities.map((activity) => {
903
934
  const activityUpdate = { _id: activity._id };
@@ -905,9 +936,26 @@ exports.updateActivityTool = {
905
936
  activityUpdate.name = activity.name;
906
937
  if (activity.fields) {
907
938
  // Parse fields if string
908
- const parsedFields = typeof activity.fields === 'string'
939
+ let parsedFields = typeof activity.fields === 'string'
909
940
  ? JSON.parse(activity.fields)
910
941
  : activity.fields;
942
+ // Auto-fix activitylink fields: if LLM passes object with _id, extract just the ID
943
+ if (parsedFields && typeof parsedFields === "object") {
944
+ for (const [fieldId, fieldValue] of Object.entries(parsedFields)) {
945
+ if (fieldValue &&
946
+ typeof fieldValue === "object" &&
947
+ !Array.isArray(fieldValue) &&
948
+ "_id" in fieldValue &&
949
+ typeof fieldValue._id === "string") {
950
+ logger.warn("Auto-fixing activitylink field in bulk mode", {
951
+ activityId: activity._id,
952
+ fieldId,
953
+ extracted: fieldValue._id,
954
+ });
955
+ parsedFields[fieldId] = fieldValue._id;
956
+ }
957
+ }
958
+ }
911
959
  activityUpdate.fields = parsedFields;
912
960
  }
913
961
  return activityUpdate;
@@ -931,6 +979,30 @@ exports.updateActivityTool = {
931
979
  };
932
980
  }
933
981
  // SINGLE MODE: Update one activity
982
+ // Validate activityId is provided for single mode
983
+ if (!args.activityId || typeof args.activityId !== 'string' || args.activityId.length < 24) {
984
+ return {
985
+ content: [{
986
+ type: "text",
987
+ text: `❌ **Error: Missing or invalid activityId**\n\nFor single activity updates, you must provide a valid "activityId" (24-character hex string).\n\n**What you sent:** activityId = ${JSON.stringify(args.activityId)}\n\n**Correct format:**\n\`\`\`json\n{\n "activityId": "691ffe654217e9e8434e577c",\n "name": "New Name",\n "fields": { "fieldId": "value" }\n}\n\`\`\`\n\n**For bulk updates (3+ activities), use:**\n\`\`\`json\n{\n "activities": [\n { "_id": "activity-id-1", "fields": {...} },\n { "_id": "activity-id-2", "name": "New Name" }\n ]\n}\n\`\`\`\n\n**Tip:** Use list_activities or show_activity_by_id to find activity IDs first.`,
988
+ }],
989
+ };
990
+ }
991
+ // Validate that at least one update field is provided
992
+ if (!args.name && !args.fields && !args.phaseId) {
993
+ return {
994
+ content: [{
995
+ type: "text",
996
+ text: `❌ **Error: No update data provided**\n\nYou called update_activity with activityId="${args.activityId}" but didn't provide any data to update.\n\n**You must provide at least one of:**\n- "name" - to update the activity name\n- "fields" - to update field values\n- "phaseId" - to move to a different phase\n\n**Example:**\n\`\`\`json\n{\n "activityId": "${args.activityId}",\n "fields": {\n "fieldId123": "new value"\n }\n}\n\`\`\``,
997
+ }],
998
+ };
999
+ }
1000
+ logger.debug("Single update mode", {
1001
+ activityId: args.activityId,
1002
+ name: args.name,
1003
+ fields: JSON.stringify(args.fields, null, 2),
1004
+ phaseId: args.phaseId
1005
+ });
934
1006
  const updates = buildActivityUpdate(args, context);
935
1007
  const options = args.phaseId ? { phaseId: args.phaseId } : undefined;
936
1008
  const result = await context.hailer.updateActivities([updates], options);
@@ -18,47 +18,7 @@ const logger = (0, logger_1.createLogger)({ component: 'app-core' });
18
18
  // ============================================================================
19
19
  // CREATE APP TOOL
20
20
  // ============================================================================
21
- const createAppDescription = `🧪 [PLAYGROUND] Create App - Create Hailer app entry in workspace
22
-
23
- **What are Apps?**
24
- Apps are custom web applications that extend Hailer workspace functionality. Think React/vanilla JS apps running within Hailer.
25
-
26
- **App Types**:
27
- - **Development Apps**: URL points to localhost (e.g., http://localhost:3000)
28
- - **Published Apps**: URL empty or points to production
29
-
30
- **Example 1 - Development App**:
31
- \`\`\`javascript
32
- create_app({
33
- name: 'Local Development',
34
- description: 'App pointing to local server',
35
- url: 'http://localhost:3000'
36
- })
37
- \`\`\`
38
-
39
- **Example 2 - Published App**:
40
- \`\`\`javascript
41
- create_app({
42
- name: 'Task Manager',
43
- description: 'Production task management app',
44
- url: '' // Empty for auto URL
45
- })
46
- \`\`\`
47
-
48
- **Properties**:
49
- - \`name\` (required) - App display name
50
- - \`description\` (optional) - App description
51
- - \`url\` (optional) - App URL or empty for auto-generated
52
- - \`image\` (optional) - Image/icon ID
53
-
54
- **Requirements**:
55
- - User must be workspace administrator
56
- - For published apps, leave URL empty
57
-
58
- **Tips**:
59
- - Use development apps for local testing
60
- - Published apps get automatic URLs from Hailer
61
- - Use \`add_app_member\` to share with others`;
21
+ const createAppDescription = `Create Hailer app entry in workspace`;
62
22
  exports.createAppTool = {
63
23
  name: 'create_app',
64
24
  group: tool_registry_1.ToolGroup.PLAYGROUND,
@@ -176,28 +136,7 @@ exports.createAppTool = {
176
136
  // ============================================================================
177
137
  // LIST APPS TOOL
178
138
  // ============================================================================
179
- const listAppsDescription = `🧪 [PLAYGROUND] List Apps - View all apps in workspace
180
-
181
- **What it does**:
182
- Lists all Hailer apps in the current workspace that you have access to.
183
-
184
- **Example**:
185
- \`\`\`javascript
186
- list_apps()
187
- \`\`\`
188
-
189
- **Shows**:
190
- - App name and description
191
- - App ID
192
- - URL (localhost for dev, auto for published)
193
- - Creator and timestamps
194
- - Configuration
195
-
196
- **Use Cases**:
197
- - See all workspace apps
198
- - Find app IDs for updates
199
- - Check which apps are development vs published
200
- - Audit app configuration`;
139
+ const listAppsDescription = `List all apps in workspace`;
201
140
  exports.listAppsTool = {
202
141
  name: 'list_apps',
203
142
  group: tool_registry_1.ToolGroup.PLAYGROUND,
@@ -290,41 +229,7 @@ exports.listAppsTool = {
290
229
  // ============================================================================
291
230
  // UPDATE APP TOOL
292
231
  // ============================================================================
293
- const updateAppDescription = `🧪 [PLAYGROUND] Update App - Modify app properties
294
-
295
- **What it does**:
296
- Updates an existing app's properties (name, description, URL, etc.).
297
-
298
- **Example - Update name and description**:
299
- \`\`\`javascript
300
- update_app({
301
- appId: '<app-id>',
302
- name: 'Updated App Name',
303
- description: 'New description'
304
- })
305
- \`\`\`
306
-
307
- **Example - Change URL (dev to prod)**:
308
- \`\`\`javascript
309
- update_app({
310
- appId: '<app-id>',
311
- url: '' // Empty for published
312
- })
313
- \`\`\`
314
-
315
- **Updatable Properties**:
316
- - \`name\` - App display name
317
- - \`description\` - App description
318
- - \`url\` - App URL
319
- - \`image\` - App icon ID
320
- - \`config\` - App configuration
321
-
322
- **Requirements**:
323
- - User must be app creator or workspace admin
324
-
325
- **Tips**:
326
- - Only specified properties are updated
327
- - Use \`list_apps\` to get app IDs`;
232
+ const updateAppDescription = `Update app properties`;
328
233
  exports.updateAppTool = {
329
234
  name: 'update_app',
330
235
  group: tool_registry_1.ToolGroup.PLAYGROUND,
@@ -437,48 +342,7 @@ exports.updateAppTool = {
437
342
  // ============================================================================
438
343
  // REMOVE APP TOOL
439
344
  // ============================================================================
440
- const removeAppDescription = `🧪 [PLAYGROUND] Remove App - ⚠️ DANGER: Permanently deletes app entry from Hailer
441
-
442
- ⚠️ **MANDATORY: GATHER COMPLETE CONTEXT BEFORE CALLING THIS TOOL**
443
- **BEFORE calling this tool, you are REQUIRED to:**
444
- 1. Load the skill: \`get_skill({ skillName: "remove-app-skill" })\`
445
- 2. Fetch app details with \`list_apps\` to get:
446
- - App name and ID
447
- - Workspace ID and name
448
- - App type (development/published)
449
- - App configuration
450
- 3. Show comprehensive confirmation message including:
451
- - Workspace ID and name
452
- - App ID and name
453
- - What will be deleted (entry, permissions, configuration)
454
- - What won't be deleted (published code remains)
455
- - Clear irreversibility warning
456
- 4. Wait for explicit user confirmation
457
- **FAILURE TO GATHER AND SHOW THIS CONTEXT IS AN ERROR**
458
-
459
- **Required**: appId
460
- **Permission**: App creator or workspace administrator
461
-
462
- **What gets deleted**:
463
- - App entry (name, description, URL, metadata)
464
- - App permissions
465
- - App configuration
466
-
467
- **What doesn't get deleted**:
468
- - Published app code (remains on server)
469
- - App data (if any)
470
-
471
- **Example**:
472
- \`\`\`javascript
473
- remove_app({
474
- appId: '<app-id>'
475
- })
476
- \`\`\`
477
-
478
- **Tips**:
479
- - Use \`list_apps\` to get app IDs
480
- - Published app code must be removed separately
481
- - Operation cannot be undone`;
345
+ const removeAppDescription = `Delete app permanently`;
482
346
  exports.removeAppTool = {
483
347
  name: 'remove_app',
484
348
  group: tool_registry_1.ToolGroup.NUCLEAR,