@troykelly/openclaw-projects 0.0.23 → 0.0.25

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.
@@ -500,28 +500,161 @@ const contactGetSchema = {
500
500
  const contactCreateSchema = {
501
501
  type: 'object',
502
502
  properties: {
503
- name: {
503
+ display_name: {
504
504
  type: 'string',
505
- description: 'Contact name',
506
- minLength: 1,
505
+ description: 'Full display name (required for organizations/groups, optional for persons if given_name or family_name provided)',
507
506
  maxLength: 200,
508
507
  },
508
+ given_name: {
509
+ type: 'string',
510
+ description: 'Given (first) name',
511
+ maxLength: 100,
512
+ },
513
+ family_name: {
514
+ type: 'string',
515
+ description: 'Family (last) name',
516
+ maxLength: 100,
517
+ },
518
+ nickname: {
519
+ type: 'string',
520
+ description: 'Nickname or short name',
521
+ maxLength: 100,
522
+ },
523
+ contact_kind: {
524
+ type: 'string',
525
+ description: 'Contact type',
526
+ enum: ['person', 'organisation', 'group', 'agent'],
527
+ default: 'person',
528
+ },
509
529
  email: {
510
530
  type: 'string',
511
- description: 'Contact email address',
531
+ description: 'Primary email address (creates an email endpoint)',
512
532
  format: 'email',
513
533
  },
514
534
  phone: {
515
535
  type: 'string',
516
- description: 'Contact phone number',
536
+ description: 'Primary phone number (creates a phone endpoint)',
517
537
  },
518
538
  notes: {
519
539
  type: 'string',
520
540
  description: 'Notes about the contact',
521
541
  maxLength: 5000,
522
542
  },
543
+ tags: {
544
+ type: 'array',
545
+ description: 'Tags to assign to the contact (max 20)',
546
+ items: { type: 'string', maxLength: 100 },
547
+ },
548
+ },
549
+ required: [],
550
+ };
551
+ /**
552
+ * Contact update tool JSON Schema (#1600)
553
+ */
554
+ const contactUpdateSchema = {
555
+ type: 'object',
556
+ properties: {
557
+ contact_id: {
558
+ type: 'string',
559
+ description: 'ID of the contact to update',
560
+ format: 'uuid',
561
+ },
562
+ display_name: {
563
+ type: 'string',
564
+ description: 'Updated display name',
565
+ maxLength: 200,
566
+ },
567
+ given_name: { type: 'string', maxLength: 100 },
568
+ family_name: { type: 'string', maxLength: 100 },
569
+ nickname: { type: 'string', maxLength: 100 },
570
+ notes: { type: 'string', maxLength: 5000 },
571
+ tags: {
572
+ type: 'array',
573
+ description: 'Replace all tags (empty array removes all)',
574
+ items: { type: 'string', maxLength: 100 },
575
+ },
576
+ },
577
+ required: ['contact_id'],
578
+ };
579
+ /**
580
+ * Contact merge tool JSON Schema (#1600)
581
+ */
582
+ const contactMergeSchema = {
583
+ type: 'object',
584
+ properties: {
585
+ survivor_id: {
586
+ type: 'string',
587
+ description: 'ID of the contact to keep (survivor)',
588
+ format: 'uuid',
589
+ },
590
+ loser_id: {
591
+ type: 'string',
592
+ description: 'ID of the contact to merge into the survivor (will be soft-deleted)',
593
+ format: 'uuid',
594
+ },
595
+ },
596
+ required: ['survivor_id', 'loser_id'],
597
+ };
598
+ /**
599
+ * Contact tag add tool JSON Schema (#1600)
600
+ */
601
+ const contactTagAddSchema = {
602
+ type: 'object',
603
+ properties: {
604
+ contact_id: {
605
+ type: 'string',
606
+ description: 'Contact ID',
607
+ format: 'uuid',
608
+ },
609
+ tags: {
610
+ type: 'array',
611
+ description: 'Tags to add (1–20 tags)',
612
+ items: { type: 'string', maxLength: 100 },
613
+ },
614
+ },
615
+ required: ['contact_id', 'tags'],
616
+ };
617
+ /**
618
+ * Contact tag remove tool JSON Schema (#1600)
619
+ */
620
+ const contactTagRemoveSchema = {
621
+ type: 'object',
622
+ properties: {
623
+ contact_id: {
624
+ type: 'string',
625
+ description: 'Contact ID',
626
+ format: 'uuid',
627
+ },
628
+ tag: {
629
+ type: 'string',
630
+ description: 'Tag to remove',
631
+ maxLength: 100,
632
+ },
633
+ },
634
+ required: ['contact_id', 'tag'],
635
+ };
636
+ /**
637
+ * Contact resolve tool JSON Schema (#1601)
638
+ * Resolves a sender identity (phone, email, name) to a contact match.
639
+ */
640
+ const contactResolveSchema = {
641
+ type: 'object',
642
+ properties: {
643
+ phone: {
644
+ type: 'string',
645
+ description: 'Sender phone number to resolve',
646
+ },
647
+ email: {
648
+ type: 'string',
649
+ description: 'Sender email address to resolve',
650
+ format: 'email',
651
+ },
652
+ name: {
653
+ type: 'string',
654
+ description: 'Sender name for fuzzy matching',
655
+ maxLength: 200,
656
+ },
523
657
  },
524
- required: ['name'],
525
658
  };
526
659
  /**
527
660
  * SMS send tool JSON Schema
@@ -1316,7 +1449,7 @@ export async function refreshNamespacesAsync(state) {
1316
1449
  return;
1317
1450
  state.refreshInFlight = true;
1318
1451
  try {
1319
- const response = await state.apiClient.get('/api/namespaces', { user_id: state.user_id });
1452
+ const response = await state.apiClient.get('/api/namespaces', { user_id: state.user_id, user_email: state.user_email });
1320
1453
  if (!response.success) {
1321
1454
  state.logger.warn('Namespace discovery failed, keeping cached list', { error: response.error.message });
1322
1455
  // Do NOT update timestamp on failure — let the next check retry sooner
@@ -1354,7 +1487,9 @@ export async function refreshNamespacesAsync(state) {
1354
1487
  * Create tool execution handlers
1355
1488
  */
1356
1489
  function createToolHandlers(state) {
1357
- const { config, logger, apiClient, user_id, resolvedNamespace } = state;
1490
+ const { config, logger, apiClient, user_id, user_email, resolvedNamespace } = state;
1491
+ /** Build RequestOptions with user_id and user_email for identity resolution (#1567) */
1492
+ const reqOpts = () => ({ user_id, user_email });
1358
1493
  /**
1359
1494
  * Get the effective namespace for a store/create operation.
1360
1495
  * Uses explicit tool param if provided, otherwise falls back to config default.
@@ -1398,7 +1533,7 @@ function createToolHandlers(state) {
1398
1533
  const ns = getRecallNamespaces(params);
1399
1534
  if (ns.length > 0)
1400
1535
  queryParams.set('namespaces', ns.join(','));
1401
- const response = await apiClient.get(`/api/memories/search?${queryParams}`, { user_id });
1536
+ const response = await apiClient.get(`/api/memories/search?${queryParams}`, reqOpts());
1402
1537
  if (!response.success) {
1403
1538
  return { success: false, error: response.error.message };
1404
1539
  }
@@ -1484,7 +1619,7 @@ function createToolHandlers(state) {
1484
1619
  if (location.place_label)
1485
1620
  payload.place_label = location.place_label;
1486
1621
  }
1487
- const response = await apiClient.post('/api/memories/unified', payload, { user_id });
1622
+ const response = await apiClient.post('/api/memories/unified', payload, reqOpts());
1488
1623
  if (!response.success) {
1489
1624
  return { success: false, error: response.error.message };
1490
1625
  }
@@ -1505,7 +1640,7 @@ function createToolHandlers(state) {
1505
1640
  const { memory_id, query } = params;
1506
1641
  try {
1507
1642
  if (memory_id) {
1508
- const response = await apiClient.delete(`/api/memories/${memory_id}`, { user_id });
1643
+ const response = await apiClient.delete(`/api/memories/${memory_id}`, reqOpts());
1509
1644
  if (!response.success) {
1510
1645
  return { success: false, error: response.error.message };
1511
1646
  }
@@ -1521,7 +1656,7 @@ function createToolHandlers(state) {
1521
1656
  const forgetNs = getRecallNamespaces(params);
1522
1657
  if (forgetNs.length > 0)
1523
1658
  forgetQp.set('namespaces', forgetNs.join(','));
1524
- const searchResponse = await apiClient.get(`/api/memories/search?${forgetQp}`, { user_id });
1659
+ const searchResponse = await apiClient.get(`/api/memories/search?${forgetQp}`, reqOpts());
1525
1660
  if (!searchResponse.success) {
1526
1661
  return { success: false, error: searchResponse.error.message };
1527
1662
  }
@@ -1534,7 +1669,7 @@ function createToolHandlers(state) {
1534
1669
  }
1535
1670
  // Single high-confidence match → auto-delete
1536
1671
  if (matches.length === 1 && (matches[0].similarity ?? 0) > 0.9) {
1537
- const delResponse = await apiClient.delete(`/api/memories/${matches[0].id}`, { user_id });
1672
+ const delResponse = await apiClient.delete(`/api/memories/${matches[0].id}`, reqOpts());
1538
1673
  if (!delResponse.success) {
1539
1674
  return { success: false, error: delResponse.error.message };
1540
1675
  }
@@ -1574,7 +1709,7 @@ function createToolHandlers(state) {
1574
1709
  const projListNs = getRecallNamespaces(params);
1575
1710
  if (projListNs.length > 0)
1576
1711
  queryParams.set('namespaces', projListNs.join(','));
1577
- const response = await apiClient.get(`/api/work-items?${queryParams}`, { user_id });
1712
+ const response = await apiClient.get(`/api/work-items?${queryParams}`, reqOpts());
1578
1713
  if (!response.success) {
1579
1714
  return { success: false, error: response.error.message };
1580
1715
  }
@@ -1593,7 +1728,7 @@ function createToolHandlers(state) {
1593
1728
  async project_get(params) {
1594
1729
  const { project_id } = params;
1595
1730
  try {
1596
- const response = await apiClient.get(`/api/work-items/${project_id}?user_email=${encodeURIComponent(user_id)}`, { user_id });
1731
+ const response = await apiClient.get(`/api/work-items/${project_id}?user_email=${encodeURIComponent(user_id)}`, reqOpts());
1597
1732
  if (!response.success) {
1598
1733
  return { success: false, error: response.error.message };
1599
1734
  }
@@ -1614,7 +1749,7 @@ function createToolHandlers(state) {
1614
1749
  async project_create(params) {
1615
1750
  const { name, description, status = 'active', } = params;
1616
1751
  try {
1617
- const response = await apiClient.post('/api/work-items', { title: name, description, item_type: 'project', status, user_email: user_id, namespace: getStoreNamespace(params) }, { user_id });
1752
+ const response = await apiClient.post('/api/work-items', { title: name, description, item_type: 'project', status, user_email: user_id, namespace: getStoreNamespace(params) }, reqOpts());
1618
1753
  if (!response.success) {
1619
1754
  return { success: false, error: response.error.message };
1620
1755
  }
@@ -1649,7 +1784,7 @@ function createToolHandlers(state) {
1649
1784
  const todoListNs = getRecallNamespaces(params);
1650
1785
  if (todoListNs.length > 0)
1651
1786
  queryParams.set('namespaces', todoListNs.join(','));
1652
- const response = await apiClient.get(`/api/work-items?${queryParams}`, { user_id });
1787
+ const response = await apiClient.get(`/api/work-items?${queryParams}`, reqOpts());
1653
1788
  if (!response.success) {
1654
1789
  return { success: false, error: response.error.message };
1655
1790
  }
@@ -1686,7 +1821,7 @@ function createToolHandlers(state) {
1686
1821
  body.parent_work_item_id = project_id;
1687
1822
  if (dueDate)
1688
1823
  body.not_after = dueDate;
1689
- const response = await apiClient.post('/api/work-items', body, { user_id });
1824
+ const response = await apiClient.post('/api/work-items', body, reqOpts());
1690
1825
  if (!response.success) {
1691
1826
  return { success: false, error: response.error.message };
1692
1827
  }
@@ -1706,7 +1841,7 @@ function createToolHandlers(state) {
1706
1841
  async todo_complete(params) {
1707
1842
  const { todoId } = params;
1708
1843
  try {
1709
- const response = await apiClient.patch(`/api/work-items/${todoId}/status?user_email=${encodeURIComponent(user_id)}`, { status: 'completed' }, { user_id });
1844
+ const response = await apiClient.patch(`/api/work-items/${todoId}/status?user_email=${encodeURIComponent(user_id)}`, { status: 'completed' }, reqOpts());
1710
1845
  if (!response.success) {
1711
1846
  return { success: false, error: response.error.message };
1712
1847
  }
@@ -1739,7 +1874,7 @@ function createToolHandlers(state) {
1739
1874
  const todoSearchNs = getRecallNamespaces(params);
1740
1875
  if (todoSearchNs.length > 0)
1741
1876
  queryParams.set('namespaces', todoSearchNs.join(','));
1742
- const response = await apiClient.get(`/api/search?${queryParams}`, { user_id });
1877
+ const response = await apiClient.get(`/api/search?${queryParams}`, reqOpts());
1743
1878
  if (!response.success) {
1744
1879
  return { success: false, error: response.error.message };
1745
1880
  }
@@ -1829,7 +1964,7 @@ function createToolHandlers(state) {
1829
1964
  async contact_get(params) {
1830
1965
  const { contact_id } = params;
1831
1966
  try {
1832
- const response = await apiClient.get(`/api/contacts/${contact_id}?user_email=${encodeURIComponent(user_id)}`, { user_id });
1967
+ const response = await apiClient.get(`/api/contacts/${contact_id}?user_email=${encodeURIComponent(user_id)}`, reqOpts());
1833
1968
  if (!response.success) {
1834
1969
  return { success: false, error: response.error.message };
1835
1970
  }
@@ -1852,10 +1987,37 @@ function createToolHandlers(state) {
1852
1987
  }
1853
1988
  },
1854
1989
  async contact_create(params) {
1855
- const { name, notes } = params;
1990
+ const { display_name, given_name, family_name, nickname, contact_kind, email, phone, notes, tags } = params;
1991
+ // Must have display_name or at least given_name/family_name
1992
+ const name = display_name || [given_name, family_name].filter(Boolean).join(' ');
1993
+ if (!name) {
1994
+ return { success: false, error: 'Either display_name or given_name/family_name is required' };
1995
+ }
1856
1996
  try {
1857
- // API requires display_name, not name. Email/phone are stored as separate contact_endpoint records.
1858
- const response = await apiClient.post('/api/contacts', { display_name: name, notes, user_email: user_id, namespace: getStoreNamespace(params) }, { user_id });
1997
+ const body = {
1998
+ user_email: user_id,
1999
+ namespace: getStoreNamespace(params),
2000
+ notes,
2001
+ contact_kind: contact_kind ?? 'person',
2002
+ tags,
2003
+ };
2004
+ if (display_name)
2005
+ body.display_name = display_name;
2006
+ if (given_name)
2007
+ body.given_name = given_name;
2008
+ if (family_name)
2009
+ body.family_name = family_name;
2010
+ if (nickname)
2011
+ body.nickname = nickname;
2012
+ // Build endpoints array from convenience fields
2013
+ const endpoints = [];
2014
+ if (email)
2015
+ endpoints.push({ type: 'email', value: email });
2016
+ if (phone)
2017
+ endpoints.push({ type: 'phone', value: phone });
2018
+ if (endpoints.length > 0)
2019
+ body.endpoints = endpoints;
2020
+ const response = await apiClient.post('/api/contacts', body, reqOpts());
1859
2021
  if (!response.success) {
1860
2022
  return { success: false, error: response.error.message };
1861
2023
  }
@@ -1863,7 +2025,7 @@ function createToolHandlers(state) {
1863
2025
  success: true,
1864
2026
  data: {
1865
2027
  content: `Contact "${name}" created successfully (ID: ${response.data.id})`,
1866
- details: { id: response.data.id },
2028
+ details: { id: response.data.id, display_name: response.data.display_name ?? name },
1867
2029
  },
1868
2030
  };
1869
2031
  }
@@ -1872,6 +2034,147 @@ function createToolHandlers(state) {
1872
2034
  return { success: false, error: 'Failed to create contact' };
1873
2035
  }
1874
2036
  },
2037
+ async contact_update(params) {
2038
+ const { contact_id, ...updates } = params;
2039
+ if (!contact_id)
2040
+ return { success: false, error: 'contact_id is required' };
2041
+ try {
2042
+ const body = { namespace: getStoreNamespace(params) };
2043
+ for (const [k, v] of Object.entries(updates)) {
2044
+ if (k !== 'namespace' && v !== undefined)
2045
+ body[k] = v;
2046
+ }
2047
+ const response = await apiClient.patch(`/api/contacts/${contact_id}`, body, reqOpts());
2048
+ if (!response.success) {
2049
+ return { success: false, error: response.error.message };
2050
+ }
2051
+ return {
2052
+ success: true,
2053
+ data: {
2054
+ content: `Contact ${contact_id} updated successfully`,
2055
+ details: { id: contact_id, display_name: response.data.display_name },
2056
+ },
2057
+ };
2058
+ }
2059
+ catch (error) {
2060
+ logger.error('contact_update failed', { error });
2061
+ return { success: false, error: 'Failed to update contact' };
2062
+ }
2063
+ },
2064
+ async contact_merge(params) {
2065
+ const { survivor_id, loser_id } = params;
2066
+ if (!survivor_id || !loser_id)
2067
+ return { success: false, error: 'survivor_id and loser_id are required' };
2068
+ try {
2069
+ const response = await apiClient.post('/api/contacts/merge', { survivor_id, loser_id, namespace: getStoreNamespace(params) }, reqOpts());
2070
+ if (!response.success) {
2071
+ return { success: false, error: response.error.message };
2072
+ }
2073
+ return {
2074
+ success: true,
2075
+ data: {
2076
+ content: `Contacts merged successfully. Survivor: ${survivor_id}`,
2077
+ details: response.data,
2078
+ },
2079
+ };
2080
+ }
2081
+ catch (error) {
2082
+ logger.error('contact_merge failed', { error });
2083
+ return { success: false, error: 'Failed to merge contacts' };
2084
+ }
2085
+ },
2086
+ async contact_tag_add(params) {
2087
+ const { contact_id, tags } = params;
2088
+ if (!contact_id || !tags?.length)
2089
+ return { success: false, error: 'contact_id and tags are required' };
2090
+ try {
2091
+ const response = await apiClient.post(`/api/contacts/${contact_id}/tags`, { tags }, reqOpts());
2092
+ if (!response.success) {
2093
+ return { success: false, error: response.error.message };
2094
+ }
2095
+ return {
2096
+ success: true,
2097
+ data: {
2098
+ content: `Added ${tags.length} tag(s) to contact ${contact_id}: ${tags.join(', ')}`,
2099
+ details: { contact_id, tags },
2100
+ },
2101
+ };
2102
+ }
2103
+ catch (error) {
2104
+ logger.error('contact_tag_add failed', { error });
2105
+ return { success: false, error: 'Failed to add tags' };
2106
+ }
2107
+ },
2108
+ async contact_tag_remove(params) {
2109
+ const { contact_id, tag } = params;
2110
+ if (!contact_id || !tag)
2111
+ return { success: false, error: 'contact_id and tag are required' };
2112
+ try {
2113
+ const response = await apiClient.delete(`/api/contacts/${contact_id}/tags/${encodeURIComponent(tag)}`, reqOpts());
2114
+ if (!response.success) {
2115
+ return { success: false, error: response.error.message };
2116
+ }
2117
+ return {
2118
+ success: true,
2119
+ data: {
2120
+ content: `Removed tag "${tag}" from contact ${contact_id}`,
2121
+ details: { contact_id, tag },
2122
+ },
2123
+ };
2124
+ }
2125
+ catch (error) {
2126
+ logger.error('contact_tag_remove failed', { error });
2127
+ return { success: false, error: 'Failed to remove tag' };
2128
+ }
2129
+ },
2130
+ async contact_resolve(params) {
2131
+ const { phone, email, name } = params;
2132
+ if (!phone && !email && !name) {
2133
+ return { success: false, error: 'At least one of phone, email, or name is required' };
2134
+ }
2135
+ try {
2136
+ const queryParams = new URLSearchParams();
2137
+ if (phone)
2138
+ queryParams.set('phone', phone);
2139
+ if (email)
2140
+ queryParams.set('email', email);
2141
+ if (name)
2142
+ queryParams.set('name', name);
2143
+ const response = await apiClient.get(`/api/contacts/suggest-match?${queryParams}`, reqOpts());
2144
+ if (!response.success) {
2145
+ return { success: false, error: response.error.message };
2146
+ }
2147
+ const matches = response.data.matches ?? [];
2148
+ if (matches.length === 0) {
2149
+ return {
2150
+ success: true,
2151
+ data: {
2152
+ content: 'No matching contacts found for the provided sender information.',
2153
+ details: { matches: [], resolved: false },
2154
+ },
2155
+ };
2156
+ }
2157
+ const best = matches[0];
2158
+ const content = matches
2159
+ .map((m) => `- ${m.display_name} (${Math.round(m.confidence * 100)}% match, ID: ${m.contact_id})`)
2160
+ .join('\n');
2161
+ return {
2162
+ success: true,
2163
+ data: {
2164
+ content: `Found ${matches.length} matching contact(s):\n${content}`,
2165
+ details: {
2166
+ matches,
2167
+ resolved: best.confidence >= 0.8,
2168
+ best_match: best,
2169
+ },
2170
+ },
2171
+ };
2172
+ }
2173
+ catch (error) {
2174
+ logger.error('contact_resolve failed', { error });
2175
+ return { success: false, error: 'Failed to resolve sender identity' };
2176
+ }
2177
+ },
1875
2178
  async sms_send(params) {
1876
2179
  const { to, body, idempotency_key } = params;
1877
2180
  // Check Twilio configuration
@@ -1908,7 +2211,7 @@ function createToolHandlers(state) {
1908
2211
  hasIdempotencyKey: !!idempotency_key,
1909
2212
  });
1910
2213
  try {
1911
- const response = await apiClient.post('/api/twilio/sms/send', { to, body, idempotency_key }, { user_id });
2214
+ const response = await apiClient.post('/api/twilio/sms/send', { to, body, idempotency_key }, reqOpts());
1912
2215
  if (!response.success) {
1913
2216
  logger.error('sms_send API error', {
1914
2217
  user_id,
@@ -1989,7 +2292,7 @@ function createToolHandlers(state) {
1989
2292
  hasIdempotencyKey: !!idempotency_key,
1990
2293
  });
1991
2294
  try {
1992
- const response = await apiClient.post('/api/postmark/email/send', { to, subject, body, html_body, thread_id, idempotency_key }, { user_id });
2295
+ const response = await apiClient.post('/api/postmark/email/send', { to, subject, body, html_body, thread_id, idempotency_key }, reqOpts());
1993
2296
  if (!response.success) {
1994
2297
  logger.error('email_send API error', {
1995
2298
  user_id,
@@ -2064,7 +2367,7 @@ function createToolHandlers(state) {
2064
2367
  queryParams.set('include_thread', 'true');
2065
2368
  }
2066
2369
  // Unified search API: returns { results: [{ type, id, title, snippet, score, metadata }], total }
2067
- const response = await apiClient.get(`/api/search?${queryParams}`, { user_id });
2370
+ const response = await apiClient.get(`/api/search?${queryParams}`, reqOpts());
2068
2371
  if (!response.success) {
2069
2372
  logger.error('message_search API error', {
2070
2373
  user_id,
@@ -2177,7 +2480,7 @@ function createToolHandlers(state) {
2177
2480
  if (contact_id) {
2178
2481
  queryParams.set('contact_id', contact_id);
2179
2482
  }
2180
- const response = await apiClient.get(`/api/search?${queryParams}`, { user_id });
2483
+ const response = await apiClient.get(`/api/search?${queryParams}`, reqOpts());
2181
2484
  if (!response.success) {
2182
2485
  logger.error('thread_list API error', {
2183
2486
  user_id,
@@ -2254,7 +2557,7 @@ function createToolHandlers(state) {
2254
2557
  try {
2255
2558
  const queryParams = new URLSearchParams();
2256
2559
  queryParams.set('limit', String(message_limit));
2257
- const response = await apiClient.get(`/api/threads/${thread_id}/history?${queryParams}`, { user_id });
2560
+ const response = await apiClient.get(`/api/threads/${thread_id}/history?${queryParams}`, reqOpts());
2258
2561
  if (!response.success) {
2259
2562
  logger.error('thread_get API error', {
2260
2563
  user_id,
@@ -2362,7 +2665,7 @@ function createToolHandlers(state) {
2362
2665
  if (notes) {
2363
2666
  body.notes = notes;
2364
2667
  }
2365
- const response = await apiClient.post('/api/relationships/set', body, { user_id });
2668
+ const response = await apiClient.post('/api/relationships/set', body, reqOpts());
2366
2669
  if (!response.success) {
2367
2670
  return { success: false, error: response.error.message };
2368
2671
  }
@@ -2413,7 +2716,7 @@ function createToolHandlers(state) {
2413
2716
  else {
2414
2717
  // Search for contact by name (Issue #1172: scope by user_email)
2415
2718
  const searchParams = new URLSearchParams({ search: contact, limit: '1', user_email: user_id });
2416
- const searchResponse = await apiClient.get(`/api/contacts?${searchParams}`, { user_id });
2719
+ const searchResponse = await apiClient.get(`/api/contacts?${searchParams}`, reqOpts());
2417
2720
  if (!searchResponse.success) {
2418
2721
  return { success: false, error: searchResponse.error.message };
2419
2722
  }
@@ -2424,7 +2727,7 @@ function createToolHandlers(state) {
2424
2727
  contact_id = contacts[0].id;
2425
2728
  }
2426
2729
  // Use graph traversal endpoint which returns related_contacts
2427
- const response = await apiClient.get(`/api/contacts/${contact_id}/relationships?user_email=${encodeURIComponent(user_id)}`, { user_id });
2730
+ const response = await apiClient.get(`/api/contacts/${contact_id}/relationships?user_email=${encodeURIComponent(user_id)}`, reqOpts());
2428
2731
  if (!response.success) {
2429
2732
  if (response.error.code === 'NOT_FOUND') {
2430
2733
  return { success: false, error: 'Contact not found.' };
@@ -2492,7 +2795,7 @@ function createToolHandlers(state) {
2492
2795
  if (maxDownloads !== undefined) {
2493
2796
  body.max_downloads = maxDownloads;
2494
2797
  }
2495
- const response = await apiClient.post(`/api/files/${fileId}/share`, body, { user_id });
2798
+ const response = await apiClient.post(`/api/files/${fileId}/share`, body, reqOpts());
2496
2799
  if (!response.success) {
2497
2800
  logger.error('file_share API error', {
2498
2801
  user_id,
@@ -2604,7 +2907,7 @@ function createToolHandlers(state) {
2604
2907
  const queryParams = new URLSearchParams({ limit: String(limit), offset: String(offset) });
2605
2908
  if (channel_type)
2606
2909
  queryParams.set('channel_type', channel_type);
2607
- const response = await apiClient.get(`/api/prompt-templates?${queryParams.toString()}`, { user_id });
2910
+ const response = await apiClient.get(`/api/prompt-templates?${queryParams.toString()}`, reqOpts());
2608
2911
  if (!response.success) {
2609
2912
  return { success: false, error: response.error.message || 'Failed to list prompt templates' };
2610
2913
  }
@@ -2622,7 +2925,7 @@ function createToolHandlers(state) {
2622
2925
  async prompt_template_get(params) {
2623
2926
  const { id } = params;
2624
2927
  try {
2625
- const response = await apiClient.get(`/api/prompt-templates/${id}`, { user_id });
2928
+ const response = await apiClient.get(`/api/prompt-templates/${id}`, reqOpts());
2626
2929
  if (!response.success) {
2627
2930
  return { success: false, error: response.error.message || 'Prompt template not found' };
2628
2931
  }
@@ -2637,7 +2940,7 @@ function createToolHandlers(state) {
2637
2940
  async prompt_template_create(params) {
2638
2941
  const { label, content, channel_type, is_default } = params;
2639
2942
  try {
2640
- const response = await apiClient.post('/api/prompt-templates', { label, content, channel_type, is_default }, { user_id });
2943
+ const response = await apiClient.post('/api/prompt-templates', { label, content, channel_type, is_default }, reqOpts());
2641
2944
  if (!response.success) {
2642
2945
  return { success: false, error: response.error.message || 'Failed to create prompt template' };
2643
2946
  }
@@ -2651,7 +2954,7 @@ function createToolHandlers(state) {
2651
2954
  async prompt_template_update(params) {
2652
2955
  const { id, ...updates } = params;
2653
2956
  try {
2654
- const response = await apiClient.put(`/api/prompt-templates/${id}`, updates, { user_id });
2957
+ const response = await apiClient.put(`/api/prompt-templates/${id}`, updates, reqOpts());
2655
2958
  if (!response.success) {
2656
2959
  return { success: false, error: response.error.message || 'Failed to update prompt template' };
2657
2960
  }
@@ -2665,7 +2968,7 @@ function createToolHandlers(state) {
2665
2968
  async prompt_template_delete(params) {
2666
2969
  const { id } = params;
2667
2970
  try {
2668
- const response = await apiClient.delete(`/api/prompt-templates/${id}`, { user_id });
2971
+ const response = await apiClient.delete(`/api/prompt-templates/${id}`, reqOpts());
2669
2972
  if (!response.success) {
2670
2973
  return { success: false, error: response.error.message || 'Failed to delete prompt template' };
2671
2974
  }
@@ -2689,7 +2992,7 @@ function createToolHandlers(state) {
2689
2992
  queryParams.set('limit', String(limit));
2690
2993
  if (offset !== undefined)
2691
2994
  queryParams.set('offset', String(offset));
2692
- const response = await apiClient.get(`/api/inbound-destinations?${queryParams.toString()}`, { user_id });
2995
+ const response = await apiClient.get(`/api/inbound-destinations?${queryParams.toString()}`, reqOpts());
2693
2996
  if (!response.success) {
2694
2997
  return { success: false, error: response.error.message || 'Failed to list inbound destinations' };
2695
2998
  }
@@ -2707,7 +3010,7 @@ function createToolHandlers(state) {
2707
3010
  async inbound_destination_get(params) {
2708
3011
  const { id } = params;
2709
3012
  try {
2710
- const response = await apiClient.get(`/api/inbound-destinations/${id}`, { user_id });
3013
+ const response = await apiClient.get(`/api/inbound-destinations/${id}`, reqOpts());
2711
3014
  if (!response.success) {
2712
3015
  return { success: false, error: response.error.message || 'Inbound destination not found' };
2713
3016
  }
@@ -2731,7 +3034,7 @@ function createToolHandlers(state) {
2731
3034
  async inbound_destination_update(params) {
2732
3035
  const { id, ...updates } = params;
2733
3036
  try {
2734
- const response = await apiClient.put(`/api/inbound-destinations/${id}`, updates, { user_id });
3037
+ const response = await apiClient.put(`/api/inbound-destinations/${id}`, updates, reqOpts());
2735
3038
  if (!response.success) {
2736
3039
  return { success: false, error: response.error.message || 'Failed to update inbound destination' };
2737
3040
  }
@@ -2745,7 +3048,7 @@ function createToolHandlers(state) {
2745
3048
  // ── Channel Default tools (Issue #1501) ──────────────────
2746
3049
  async channel_default_list() {
2747
3050
  try {
2748
- const response = await apiClient.get('/api/channel-defaults', { user_id });
3051
+ const response = await apiClient.get('/api/channel-defaults', reqOpts());
2749
3052
  if (!response.success) {
2750
3053
  return { success: false, error: response.error.message || 'Failed to list channel defaults' };
2751
3054
  }
@@ -2763,7 +3066,7 @@ function createToolHandlers(state) {
2763
3066
  async channel_default_get(params) {
2764
3067
  const { channel_type } = params;
2765
3068
  try {
2766
- const response = await apiClient.get(`/api/channel-defaults/${channel_type}`, { user_id });
3069
+ const response = await apiClient.get(`/api/channel-defaults/${channel_type}`, reqOpts());
2767
3070
  if (!response.success) {
2768
3071
  return { success: false, error: response.error.message || 'Channel default not found' };
2769
3072
  }
@@ -2783,7 +3086,7 @@ function createToolHandlers(state) {
2783
3086
  async channel_default_set(params) {
2784
3087
  const { channel_type, agent_id, prompt_template_id, context_id } = params;
2785
3088
  try {
2786
- const response = await apiClient.put(`/api/channel-defaults/${channel_type}`, { agent_id, prompt_template_id, context_id }, { user_id });
3089
+ const response = await apiClient.put(`/api/channel-defaults/${channel_type}`, { agent_id, prompt_template_id, context_id }, reqOpts());
2787
3090
  if (!response.success) {
2788
3091
  return { success: false, error: response.error.message || 'Failed to set channel default' };
2789
3092
  }
@@ -2797,7 +3100,7 @@ function createToolHandlers(state) {
2797
3100
  // ── Namespace management handlers (Issue #1536) ──────────────
2798
3101
  async namespace_list() {
2799
3102
  try {
2800
- const response = await apiClient.get('/api/namespaces', { user_id });
3103
+ const response = await apiClient.get('/api/namespaces', reqOpts());
2801
3104
  if (!response.success) {
2802
3105
  return { success: false, error: response.error.message || 'Failed to list namespaces' };
2803
3106
  }
@@ -2827,7 +3130,7 @@ function createToolHandlers(state) {
2827
3130
  async namespace_create(params) {
2828
3131
  const { name } = params;
2829
3132
  try {
2830
- const response = await apiClient.post('/api/namespaces', { name }, { user_id });
3133
+ const response = await apiClient.post('/api/namespaces', { name }, reqOpts());
2831
3134
  if (!response.success) {
2832
3135
  return { success: false, error: response.error.message || 'Failed to create namespace' };
2833
3136
  }
@@ -2841,7 +3144,7 @@ function createToolHandlers(state) {
2841
3144
  async namespace_grant(params) {
2842
3145
  const { namespace, email, role, is_default } = params;
2843
3146
  try {
2844
- const response = await apiClient.post(`/api/namespaces/${encodeURIComponent(namespace)}/grants`, { email, role: role || 'member', is_default: is_default ?? false }, { user_id });
3147
+ const response = await apiClient.post(`/api/namespaces/${encodeURIComponent(namespace)}/grants`, { email, role: role || 'member', is_default: is_default ?? false }, reqOpts());
2845
3148
  if (!response.success) {
2846
3149
  return { success: false, error: response.error.message || 'Failed to grant namespace access' };
2847
3150
  }
@@ -2856,7 +3159,7 @@ function createToolHandlers(state) {
2856
3159
  async namespace_members(params) {
2857
3160
  const { namespace } = params;
2858
3161
  try {
2859
- const response = await apiClient.get(`/api/namespaces/${encodeURIComponent(namespace)}`, { user_id });
3162
+ const response = await apiClient.get(`/api/namespaces/${encodeURIComponent(namespace)}`, reqOpts());
2860
3163
  if (!response.success) {
2861
3164
  return { success: false, error: response.error.message || 'Failed to list namespace members' };
2862
3165
  }
@@ -2875,7 +3178,7 @@ function createToolHandlers(state) {
2875
3178
  async namespace_revoke(params) {
2876
3179
  const { namespace, grant_id } = params;
2877
3180
  try {
2878
- const response = await apiClient.delete(`/api/namespaces/${encodeURIComponent(namespace)}/grants/${encodeURIComponent(grant_id)}`, { user_id });
3181
+ const response = await apiClient.delete(`/api/namespaces/${encodeURIComponent(namespace)}/grants/${encodeURIComponent(grant_id)}`, reqOpts());
2879
3182
  if (!response.success) {
2880
3183
  return { success: false, error: response.error.message || 'Failed to revoke namespace access' };
2881
3184
  }
@@ -2945,8 +3248,12 @@ export const registerOpenClaw = (api) => {
2945
3248
  recallNamespaces: resolvedNamespace.recall,
2946
3249
  hasStaticRecall,
2947
3250
  });
3251
+ // Extract user email from runtime context for identity resolution (#1567).
3252
+ // The agent ID (user_id) may be a short name like "troy" which doesn't match
3253
+ // user_setting.email. The email is needed for FK-constrained operations.
3254
+ const user_email = context.user?.email;
2948
3255
  // Store plugin state
2949
- const state = { config, logger, apiClient, user_id, resolvedNamespace, hasStaticRecall, lastNamespaceRefreshMs: 0, refreshInFlight: false };
3256
+ const state = { config, logger, apiClient, user_id, user_email, resolvedNamespace, hasStaticRecall, lastNamespaceRefreshMs: 0, refreshInFlight: false };
2950
3257
  // Create tool handlers
2951
3258
  const handlers = createToolHandlers(state);
2952
3259
  // Register all 30 tools with correct OpenClaw Gateway execute signature
@@ -3080,13 +3387,58 @@ export const registerOpenClaw = (api) => {
3080
3387
  },
3081
3388
  {
3082
3389
  name: 'contact_create',
3083
- description: 'Create a new contact. Use when the user mentions someone new to track.',
3390
+ description: 'Create a new contact. Supports structured names (given_name, family_name) or display_name. Optionally include email, phone, tags.',
3084
3391
  parameters: withNamespace(contactCreateSchema),
3085
3392
  execute: async (_toolCallId, params, _signal, _onUpdate) => {
3086
3393
  const result = await handlers.contact_create(params);
3087
3394
  return toAgentToolResult(result);
3088
3395
  },
3089
3396
  },
3397
+ {
3398
+ name: 'contact_update',
3399
+ description: 'Update an existing contact. Can change name, notes, tags, and other fields.',
3400
+ parameters: withNamespace(contactUpdateSchema),
3401
+ execute: async (_toolCallId, params, _signal, _onUpdate) => {
3402
+ const result = await handlers.contact_update(params);
3403
+ return toAgentToolResult(result);
3404
+ },
3405
+ },
3406
+ {
3407
+ name: 'contact_merge',
3408
+ description: 'Merge two contacts into one. The survivor keeps all data; the loser is soft-deleted. Use when duplicate contacts are detected.',
3409
+ parameters: withNamespace(contactMergeSchema),
3410
+ execute: async (_toolCallId, params, _signal, _onUpdate) => {
3411
+ const result = await handlers.contact_merge(params);
3412
+ return toAgentToolResult(result);
3413
+ },
3414
+ },
3415
+ {
3416
+ name: 'contact_tag_add',
3417
+ description: 'Add tags to a contact for categorization.',
3418
+ parameters: withNamespace(contactTagAddSchema),
3419
+ execute: async (_toolCallId, params, _signal, _onUpdate) => {
3420
+ const result = await handlers.contact_tag_add(params);
3421
+ return toAgentToolResult(result);
3422
+ },
3423
+ },
3424
+ {
3425
+ name: 'contact_tag_remove',
3426
+ description: 'Remove a tag from a contact.',
3427
+ parameters: withNamespace(contactTagRemoveSchema),
3428
+ execute: async (_toolCallId, params, _signal, _onUpdate) => {
3429
+ const result = await handlers.contact_tag_remove(params);
3430
+ return toAgentToolResult(result);
3431
+ },
3432
+ },
3433
+ {
3434
+ name: 'contact_resolve',
3435
+ description: 'Resolve a sender identity (phone, email, or name) to an existing contact. Use when an inbound message arrives and you need to identify who sent it.',
3436
+ parameters: withNamespaces(contactResolveSchema),
3437
+ execute: async (_toolCallId, params, _signal, _onUpdate) => {
3438
+ const result = await handlers.contact_resolve(params);
3439
+ return toAgentToolResult(result);
3440
+ },
3441
+ },
3090
3442
  {
3091
3443
  name: 'sms_send',
3092
3444
  description: 'Send an SMS message to a phone number. Use when you need to notify someone via text message. Requires the recipient phone number in E.164 format (e.g., +15551234567).',
@@ -3598,7 +3950,7 @@ export const registerOpenClaw = (api) => {
3598
3950
  .description('Show plugin status and statistics')
3599
3951
  .action(async () => {
3600
3952
  try {
3601
- const response = await apiClient.get('/api/health', { user_id });
3953
+ const response = await apiClient.get('/api/health', { user_id, user_email });
3602
3954
  if (response.success) {
3603
3955
  console.log('Plugin Status: Connected');
3604
3956
  }
@@ -3670,6 +4022,11 @@ export const schemas = {
3670
4022
  contactSearch: withNamespaces(contactSearchSchema),
3671
4023
  contactGet: withNamespaces(contactGetSchema),
3672
4024
  contactCreate: withNamespace(contactCreateSchema),
4025
+ contactUpdate: withNamespace(contactUpdateSchema),
4026
+ contactMerge: withNamespace(contactMergeSchema),
4027
+ contactTagAdd: withNamespace(contactTagAddSchema),
4028
+ contactTagRemove: withNamespace(contactTagRemoveSchema),
4029
+ contactResolve: withNamespaces(contactResolveSchema),
3673
4030
  smsSend: smsSendSchema,
3674
4031
  emailSend: emailSendSchema,
3675
4032
  messageSearch: withNamespaces(messageSearchSchema),