tide-commander 1.87.0 → 1.88.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/{BossLogsModal-S3Rke-8g.js → BossLogsModal-BXQh_FI_.js} +1 -1
- package/dist/assets/{BossSpawnModal-BjWGNCnz.js → BossSpawnModal-BevC86-H.js} +1 -1
- package/dist/assets/{ControlsModal-6yfU0XjZ.js → ControlsModal-OTv1pMsh.js} +1 -1
- package/dist/assets/{DockerLogsModal-CYq0hNz6.js → DockerLogsModal-oLpkSRWn.js} +1 -1
- package/dist/assets/{EmbeddedEditor-ZBdqRDqm.js → EmbeddedEditor-Ch6-Ql2A.js} +1 -1
- package/dist/assets/{GmailOAuthSetup-BcV5jAse.js → GmailOAuthSetup-wOwZdO76.js} +1 -1
- package/dist/assets/{GoogleOAuthSetup-DyUW_STE.js → GoogleOAuthSetup-ChVRBoAO.js} +1 -1
- package/dist/assets/{IframeModal-D9A3dUUc.js → IframeModal-BAQxs9VC.js} +1 -1
- package/dist/assets/{IntegrationsPanel-CHaNJBJW.js → IntegrationsPanel-YFcOTDdT.js} +2 -2
- package/dist/assets/{LogViewerModal-BWkbY7wa.js → LogViewerModal-CSAEl5nL.js} +1 -1
- package/dist/assets/{MonitoringModal-AZzokZAZ.js → MonitoringModal-Bgu6BN93.js} +1 -1
- package/dist/assets/{PM2LogsModal-q98eiBfq.js → PM2LogsModal-D0iQKsKn.js} +1 -1
- package/dist/assets/{RestoreArchivedAreaModal-CTxRP2qE.js → RestoreArchivedAreaModal-SDm4CHQ_.js} +1 -1
- package/dist/assets/{Scene2DCanvas-C11dztp1.js → Scene2DCanvas-kZMJfRRF.js} +1 -1
- package/dist/assets/{SceneManager-CsW9MYrD.js → SceneManager-CcMFDU1A.js} +1 -1
- package/dist/assets/{SkillsPanel-BeZr9w6E.js → SkillsPanel-DKT8TwSH.js} +1 -1
- package/dist/assets/{SpawnModal-DY_KM6lX.js → SpawnModal-DKfic48e.js} +1 -1
- package/dist/assets/{SubordinateAssignmentModal-D6RvjGX9.js → SubordinateAssignmentModal-BfSS7J4H.js} +1 -1
- package/dist/assets/{TriggerManagerPanel-BmqjXv9T.js → TriggerManagerPanel-C8zZf9rr.js} +2 -2
- package/dist/assets/{WorkflowEditorPanel-Rd5ZjJmt.js → WorkflowEditorPanel-BsEbQzL1.js} +1 -1
- package/dist/assets/{index-DHHRkTG1.js → index-BW6pasQS.js} +1 -1
- package/dist/assets/{index-DSvJOrb7.js → index-Bm2GprIp.js} +2 -2
- package/dist/assets/{index-BoORE9Q1.js → index-Cn6pTvu9.js} +1 -1
- package/dist/assets/{index-BYVHgVEo.js → index-Cq-zN7dW.js} +1 -1
- package/dist/assets/{index-BtJyOo4p.js → index-CrveGM3a.js} +1 -1
- package/dist/assets/index-DBbK5bs6.js +8 -0
- package/dist/assets/index-DeGYOmjB.js +2 -0
- package/dist/assets/{index-DRGyDtmm.js → index-JfilXCJq.js} +1 -1
- package/dist/assets/{index-BxaEkSIx.js → index-NPF6_aDv.js} +3 -3
- package/dist/assets/{main-kpU9m5LW.css → main-Cm8LaUxX.css} +1 -1
- package/dist/assets/main-CwfJmt2N.js +209 -0
- package/dist/assets/{web-DS0FHmg8.js → web-CN0zYUhV.js} +1 -1
- package/dist/assets/{web-D3zCwsS9.js → web-Dik5OZ1r.js} +1 -1
- package/dist/assets/{web-DEq3Te_H.js → web-JhU3bMzp.js} +1 -1
- package/dist/index.html +2 -2
- package/dist/src/packages/server/data/builtin-skills/explore-database.js +175 -0
- package/dist/src/packages/server/data/builtin-skills/index.js +2 -0
- package/dist/src/packages/server/data/index.js +56 -2
- package/dist/src/packages/server/index.js +2 -1
- package/dist/src/packages/server/integrations/gmail/gmail-trigger-handler.js +9 -1
- package/dist/src/packages/server/integrations/whatsapp/index.js +5 -4
- package/dist/src/packages/server/integrations/whatsapp/whatsapp-client.js +10 -0
- package/dist/src/packages/server/integrations/whatsapp/whatsapp-routes.js +68 -0
- package/dist/src/packages/server/integrations/whatsapp/whatsapp-trigger-handler.js +113 -0
- package/dist/src/packages/server/routes/database.js +221 -0
- package/dist/src/packages/server/routes/files.js +128 -18
- package/dist/src/packages/server/routes/index.js +2 -0
- package/dist/src/packages/server/services/building-service.js +41 -0
- package/dist/src/packages/server/services/database-service.js +61 -9
- package/dist/src/packages/server/services/index.js +1 -0
- package/dist/src/packages/server/services/ssh-tunnel-service.js +255 -0
- package/dist/src/packages/server/websocket/handler.js +2 -1
- package/dist/src/packages/server/websocket/handlers/database-handler.js +35 -0
- package/package.json +3 -1
- package/dist/assets/index-BOr_tbLK.js +0 -2
- package/dist/assets/index-Co7njQ0Q.js +0 -8
- package/dist/assets/main-BrZe9Zbd.js +0 -201
|
@@ -1 +1 @@
|
|
|
1
|
-
import{ck as a}from"./main-
|
|
1
|
+
import{ck as a}from"./main-CwfJmt2N.js";import{ImpactStyle as i,NotificationType as r}from"./index-Bm2GprIp.js";import"./vendor-react--Eh9ivFN.js";import"./vendor-three-Chj50gSY.js";class h extends a{constructor(){super(...arguments),this.selectionStarted=!1}async impact(t){const e=this.patternForImpact(t==null?void 0:t.style);this.vibrateWithPattern(e)}async notification(t){const e=this.patternForNotification(t==null?void 0:t.type);this.vibrateWithPattern(e)}async vibrate(t){const e=(t==null?void 0:t.duration)||300;this.vibrateWithPattern([e])}async selectionStart(){this.selectionStarted=!0}async selectionChanged(){this.selectionStarted&&this.vibrateWithPattern([70])}async selectionEnd(){this.selectionStarted=!1}patternForImpact(t=i.Heavy){return t===i.Medium?[43]:t===i.Light?[20]:[61]}patternForNotification(t=r.Success){return t===r.Warning?[30,40,30,50,60]:t===r.Error?[27,45,50]:[35,65,21]}vibrateWithPattern(t){if(navigator.vibrate)navigator.vibrate(t);else throw this.unavailable("Browser does not support the vibrate API")}}export{h as HapticsWeb};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{ck as s}from"./main-
|
|
1
|
+
import{ck as s}from"./main-CwfJmt2N.js";import"./vendor-react--Eh9ivFN.js";import"./vendor-three-Chj50gSY.js";class l extends s{constructor(){super(...arguments),this.pending=[],this.deliveredNotifications=[],this.hasNotificationSupport=()=>{if(!("Notification"in window)||!Notification.requestPermission)return!1;if(Notification.permission!=="granted")try{new Notification("")}catch(i){if(i instanceof Error&&i.name==="TypeError")return!1}return!0}}async getDeliveredNotifications(){const i=[];for(const t of this.deliveredNotifications){const e={title:t.title,id:parseInt(t.tag),body:t.body};i.push(e)}return{notifications:i}}async removeDeliveredNotifications(i){for(const t of i.notifications){const e=this.deliveredNotifications.find(n=>n.tag===String(t.id));e==null||e.close(),this.deliveredNotifications=this.deliveredNotifications.filter(()=>!e)}}async removeAllDeliveredNotifications(){for(const i of this.deliveredNotifications)i.close();this.deliveredNotifications=[]}async createChannel(){throw this.unimplemented("Not implemented on web.")}async deleteChannel(){throw this.unimplemented("Not implemented on web.")}async listChannels(){throw this.unimplemented("Not implemented on web.")}async schedule(i){if(!this.hasNotificationSupport())throw this.unavailable("Notifications not supported in this browser.");for(const t of i.notifications)this.sendNotification(t);return{notifications:i.notifications.map(t=>({id:t.id}))}}async getPending(){return{notifications:this.pending}}async registerActionTypes(){throw this.unimplemented("Not implemented on web.")}async cancel(i){this.pending=this.pending.filter(t=>!i.notifications.find(e=>e.id===t.id))}async areEnabled(){const{display:i}=await this.checkPermissions();return{value:i==="granted"}}async changeExactNotificationSetting(){throw this.unimplemented("Not implemented on web.")}async checkExactNotificationSetting(){throw this.unimplemented("Not implemented on web.")}async requestPermissions(){if(!this.hasNotificationSupport())throw this.unavailable("Notifications not supported in this browser.");return{display:this.transformNotificationPermission(await Notification.requestPermission())}}async checkPermissions(){if(!this.hasNotificationSupport())throw this.unavailable("Notifications not supported in this browser.");return{display:this.transformNotificationPermission(Notification.permission)}}transformNotificationPermission(i){switch(i){case"granted":return"granted";case"denied":return"denied";default:return"prompt"}}sendPending(){var i;const t=[],e=new Date().getTime();for(const n of this.pending)!((i=n.schedule)===null||i===void 0)&&i.at&&n.schedule.at.getTime()<=e&&(this.buildNotification(n),t.push(n));this.pending=this.pending.filter(n=>!t.find(o=>o===n))}sendNotification(i){var t;if(!((t=i.schedule)===null||t===void 0)&&t.at){const e=i.schedule.at.getTime()-new Date().getTime();this.pending.push(i),setTimeout(()=>{this.sendPending()},e);return}this.buildNotification(i)}buildNotification(i){const t=new Notification(i.title,{body:i.body,tag:String(i.id)});return t.addEventListener("click",this.onClick.bind(this,i),!1),t.addEventListener("show",this.onShow.bind(this,i),!1),t.addEventListener("close",()=>{this.deliveredNotifications=this.deliveredNotifications.filter(()=>!this)},!1),this.deliveredNotifications.push(t),t}onClick(i){const t={actionId:"tap",notification:i};this.notifyListeners("localNotificationActionPerformed",t)}onShow(i){this.notifyListeners("localNotificationReceived",i)}}export{l as LocalNotificationsWeb};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{ck as t}from"./main-
|
|
1
|
+
import{ck as t}from"./main-CwfJmt2N.js";import"./vendor-react--Eh9ivFN.js";import"./vendor-three-Chj50gSY.js";class o extends t{constructor(){super(),this.handleVisibilityChange=()=>{const e={isActive:document.hidden!==!0};this.notifyListeners("appStateChange",e),document.hidden?this.notifyListeners("pause",null):this.notifyListeners("resume",null)},document.addEventListener("visibilitychange",this.handleVisibilityChange,!1)}exitApp(){throw this.unimplemented("Not implemented on web.")}async getInfo(){throw this.unimplemented("Not implemented on web.")}async getLaunchUrl(){return{url:""}}async getState(){return{isActive:document.hidden!==!0}}async minimizeApp(){throw this.unimplemented("Not implemented on web.")}async toggleBackButtonHandler(){throw this.unimplemented("Not implemented on web.")}async getAppLanguage(){return{value:navigator.language.split("-")[0].toLowerCase()}}}export{o as AppWeb};
|
package/dist/index.html
CHANGED
|
@@ -22,10 +22,10 @@
|
|
|
22
22
|
<link rel="icon" type="image/png" sizes="16x16" href="/assets/icons/favicon-16x16.png" />
|
|
23
23
|
<link rel="apple-touch-icon" sizes="180x180" href="/assets/icons/apple-touch-icon.png" />
|
|
24
24
|
<title>Tide Commander</title>
|
|
25
|
-
<script type="module" crossorigin src="/assets/main-
|
|
25
|
+
<script type="module" crossorigin src="/assets/main-CwfJmt2N.js"></script>
|
|
26
26
|
<link rel="modulepreload" crossorigin href="/assets/vendor-react--Eh9ivFN.js">
|
|
27
27
|
<link rel="modulepreload" crossorigin href="/assets/vendor-three-Chj50gSY.js">
|
|
28
|
-
<link rel="stylesheet" crossorigin href="/assets/main-
|
|
28
|
+
<link rel="stylesheet" crossorigin href="/assets/main-Cm8LaUxX.css">
|
|
29
29
|
</head>
|
|
30
30
|
<body>
|
|
31
31
|
<div id="app"></div>
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
const BT3 = '```';
|
|
2
|
+
export const exploreDatabase = {
|
|
3
|
+
slug: 'explore-database',
|
|
4
|
+
name: 'Explore Database',
|
|
5
|
+
description: 'Inspect database buildings via the Tide Commander API: list connections, browse databases/tables, read schemas, and run SQL.',
|
|
6
|
+
allowedTools: ['Bash(curl:*)', 'Bash(jq:*)'],
|
|
7
|
+
content: `# Explore Database Skill
|
|
8
|
+
|
|
9
|
+
This skill lets you inspect any **database building** in Tide Commander through the
|
|
10
|
+
internal HTTP API. You can list connections, list databases/tables, read schemas,
|
|
11
|
+
and execute SQL — all without leaving the curl pattern.
|
|
12
|
+
|
|
13
|
+
All endpoints live under \`/api/database/\` and use the same auth header as every
|
|
14
|
+
other Tide Commander skill (\`X-Auth-Token\`). Passwords are never returned by the
|
|
15
|
+
API; responses include a \`hasPassword\` boolean instead.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Endpoint Reference
|
|
20
|
+
|
|
21
|
+
| Endpoint | Method | Purpose |
|
|
22
|
+
|---|---|---|
|
|
23
|
+
| \`/api/database/buildings\` | GET | List every database building (redacted connections) |
|
|
24
|
+
| \`/api/database/buildings/{buildingId}\` | GET | One database building with all its connections |
|
|
25
|
+
| \`/api/database/buildings/{buildingId}/connections/{connectionId}/test\` | POST | Verify the connection works |
|
|
26
|
+
| \`/api/database/buildings/{buildingId}/connections/{connectionId}/databases\` | GET | List databases on the server |
|
|
27
|
+
| \`/api/database/buildings/{buildingId}/connections/{connectionId}/databases/{database}/tables\` | GET | List tables/views in a database |
|
|
28
|
+
| \`/api/database/buildings/{buildingId}/connections/{connectionId}/databases/{database}/tables/{table}/columns\` | GET | **Describe table — columns only** (lean) |
|
|
29
|
+
| \`/api/database/buildings/{buildingId}/connections/{connectionId}/databases/{database}/tables/{table}/schema\` | GET | Full schema: columns + indexes + foreign keys |
|
|
30
|
+
| \`/api/database/buildings/{buildingId}/connections/{connectionId}/query\` | POST | Run SQL — body \`{database, query, limit?, recordHistory?}\` |
|
|
31
|
+
| \`/api/database/buildings/{buildingId}/history\` | GET | Recent query history (\`?limit=N\`) |
|
|
32
|
+
|
|
33
|
+
Engines supported: **mysql, postgresql, oracle, sqlite, mssql**. SSH tunnels are
|
|
34
|
+
applied automatically when configured on the connection.
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Step 1: Discover database buildings
|
|
39
|
+
|
|
40
|
+
${BT3}bash
|
|
41
|
+
# List all DB buildings + their connections (redacted)
|
|
42
|
+
curl -s http://localhost:5174/api/database/buildings | jq
|
|
43
|
+
|
|
44
|
+
# Compact view: just id/name/engine of each connection
|
|
45
|
+
curl -s http://localhost:5174/api/database/buildings \\
|
|
46
|
+
| jq '.buildings[] | {id, name, conns: [.connections[] | {id, engine, host, port, database}]}'
|
|
47
|
+
${BT3}
|
|
48
|
+
|
|
49
|
+
Pick the **buildingId** and **connectionId** you want to explore. The \`activeConnectionId\`
|
|
50
|
+
field hints at the one the user typically works with.
|
|
51
|
+
|
|
52
|
+
## Step 2: Verify the connection
|
|
53
|
+
|
|
54
|
+
Always run a test before any heavy query — it brings up SSH tunnels and validates
|
|
55
|
+
credentials cheaply.
|
|
56
|
+
|
|
57
|
+
${BT3}bash
|
|
58
|
+
curl -s -X POST http://localhost:5174/api/database/buildings/BUILDING_ID/connections/CONNECTION_ID/test
|
|
59
|
+
# → {"success":true,"serverVersion":"8.0.36"} or {"success":false,"error":"..."}
|
|
60
|
+
${BT3}
|
|
61
|
+
|
|
62
|
+
## Step 3: Browse databases and tables
|
|
63
|
+
|
|
64
|
+
${BT3}bash
|
|
65
|
+
# List databases on the server
|
|
66
|
+
curl -s http://localhost:5174/api/database/buildings/BUILDING_ID/connections/CONNECTION_ID/databases | jq
|
|
67
|
+
|
|
68
|
+
# List tables/views in a specific database
|
|
69
|
+
curl -s http://localhost:5174/api/database/buildings/BUILDING_ID/connections/CONNECTION_ID/databases/MY_DB/tables | jq
|
|
70
|
+
|
|
71
|
+
# Compact: just names
|
|
72
|
+
curl -s http://localhost:5174/api/database/buildings/BUILDING_ID/connections/CONNECTION_ID/databases/MY_DB/tables \\
|
|
73
|
+
| jq '.tables | map(.name)'
|
|
74
|
+
${BT3}
|
|
75
|
+
|
|
76
|
+
For Oracle, "database" means **schema/owner** (e.g. \`SCOTT\`, \`HR\`). For SQLite the
|
|
77
|
+
"database" is the file path or \`main\`.
|
|
78
|
+
|
|
79
|
+
## Step 4: Inspect a table's schema
|
|
80
|
+
|
|
81
|
+
${BT3}bash
|
|
82
|
+
curl -s http://localhost:5174/api/database/buildings/BUILDING_ID/connections/CONNECTION_ID/databases/MY_DB/tables/users/schema | jq
|
|
83
|
+
${BT3}
|
|
84
|
+
|
|
85
|
+
Returns \`{ database, table, columns, indexes, foreignKeys }\`. Use this to
|
|
86
|
+
discover columns before writing a SELECT.
|
|
87
|
+
|
|
88
|
+
## Step 5: Run SQL
|
|
89
|
+
|
|
90
|
+
${BT3}bash
|
|
91
|
+
# Read query — limit defaults to 1000, capped at 10000
|
|
92
|
+
curl -s -X POST http://localhost:5174/api/database/buildings/BUILDING_ID/connections/CONNECTION_ID/query \\
|
|
93
|
+
-H "Content-Type: application/json" \\
|
|
94
|
+
-d '{"database":"MY_DB","query":"SELECT id, email FROM users WHERE created_at > NOW() - INTERVAL 1 DAY","limit":50}'
|
|
95
|
+
|
|
96
|
+
# Persist into the building's query history (visible in the UI)
|
|
97
|
+
curl -s -X POST http://localhost:5174/api/database/buildings/BUILDING_ID/connections/CONNECTION_ID/query \\
|
|
98
|
+
-H "Content-Type: application/json" \\
|
|
99
|
+
-d '{"database":"MY_DB","query":"SELECT COUNT(*) AS n FROM users","recordHistory":true}'
|
|
100
|
+
|
|
101
|
+
# Mutating statements work too — affectedRows is reported instead of rows
|
|
102
|
+
curl -s -X POST http://localhost:5174/api/database/buildings/BUILDING_ID/connections/CONNECTION_ID/query \\
|
|
103
|
+
-H "Content-Type: application/json" \\
|
|
104
|
+
-d '{"database":"MY_DB","query":"UPDATE users SET active = 1 WHERE id = 42"}'
|
|
105
|
+
${BT3}
|
|
106
|
+
|
|
107
|
+
**Response shape (success):**
|
|
108
|
+
${BT3}json
|
|
109
|
+
{
|
|
110
|
+
"id": "q_1700000000000_abc123",
|
|
111
|
+
"status": "success",
|
|
112
|
+
"duration": 12,
|
|
113
|
+
"rows": [{"id":1,"email":"a@b.com"}],
|
|
114
|
+
"fields": [{"name":"id","type":"INT"}, {"name":"email","type":"VARCHAR"}],
|
|
115
|
+
"rowCount": 1
|
|
116
|
+
}
|
|
117
|
+
${BT3}
|
|
118
|
+
|
|
119
|
+
**Response shape (error):** \`{ "status": "error", "error": "...", "errorCode": "..." }\`
|
|
120
|
+
The HTTP status is \`400\` for SQL errors and \`500\` only for unexpected failures.
|
|
121
|
+
|
|
122
|
+
## Step 6: Review history
|
|
123
|
+
|
|
124
|
+
${BT3}bash
|
|
125
|
+
curl -s "http://localhost:5174/api/database/buildings/BUILDING_ID/history?limit=20" | jq '.history[] | {executedAt, status, duration, query}'
|
|
126
|
+
${BT3}
|
|
127
|
+
|
|
128
|
+
Only queries explicitly run with \`recordHistory:true\` (or executed via the UI)
|
|
129
|
+
appear here.
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
## Important Rules
|
|
134
|
+
|
|
135
|
+
1. **LIMIT is automatic.** SELECTs without a LIMIT/TOP/FETCH clause are wrapped
|
|
136
|
+
server-side using the \`limit\` body field (default 1000, max 10000). You don't
|
|
137
|
+
need to add it manually for safety, but you can.
|
|
138
|
+
2. **One query per request.** The execute endpoint runs a single statement; do
|
|
139
|
+
not send multi-statement scripts.
|
|
140
|
+
3. **Mutations are real.** \`UPDATE\`, \`DELETE\`, \`DROP\`, etc. execute against the
|
|
141
|
+
live connection — confirm the building/connection IDs before destructive ops.
|
|
142
|
+
4. **Engine quirks:**
|
|
143
|
+
- Oracle: pass the **schema name** as \`{database}\`.
|
|
144
|
+
- SQLite: typically \`main\`. Pragma queries work as raw SQL.
|
|
145
|
+
- SQL Server: \`TOP n\` is auto-injected for SELECTs without it.
|
|
146
|
+
5. **Never log credentials.** Connection objects from the API never include
|
|
147
|
+
passwords — \`hasPassword: true\` is the only signal.
|
|
148
|
+
|
|
149
|
+
## Example Workflow
|
|
150
|
+
|
|
151
|
+
${BT3}bash
|
|
152
|
+
# 1) Find DB buildings and pick one
|
|
153
|
+
curl -s http://localhost:5174/api/database/buildings \\
|
|
154
|
+
| jq '.buildings[] | {id, name, activeConnectionId}'
|
|
155
|
+
|
|
156
|
+
# 2) Verify
|
|
157
|
+
curl -s -X POST http://localhost:5174/api/database/buildings/building_xxx/connections/conn_yyy/test
|
|
158
|
+
|
|
159
|
+
# 3) Pick a database, list its tables
|
|
160
|
+
curl -s http://localhost:5174/api/database/buildings/building_xxx/connections/conn_yyy/databases | jq
|
|
161
|
+
curl -s http://localhost:5174/api/database/buildings/building_xxx/connections/conn_yyy/databases/app_prod/tables \\
|
|
162
|
+
| jq '.tables | map(.name)'
|
|
163
|
+
|
|
164
|
+
# 4) Inspect a table
|
|
165
|
+
curl -s http://localhost:5174/api/database/buildings/building_xxx/connections/conn_yyy/databases/app_prod/tables/orders/schema \\
|
|
166
|
+
| jq '.columns | map({name, type, primaryKey, nullable})'
|
|
167
|
+
|
|
168
|
+
# 5) Run an exploratory query
|
|
169
|
+
curl -s -X POST http://localhost:5174/api/database/buildings/building_xxx/connections/conn_yyy/query \\
|
|
170
|
+
-H "Content-Type: application/json" \\
|
|
171
|
+
-d '{"database":"app_prod","query":"SELECT status, COUNT(*) c FROM orders GROUP BY status"}' \\
|
|
172
|
+
| jq '.rows'
|
|
173
|
+
${BT3}
|
|
174
|
+
`,
|
|
175
|
+
};
|
|
@@ -13,6 +13,7 @@ import { streamingExec } from './streaming-exec.js';
|
|
|
13
13
|
import { bitbucketPR } from './bitbucket-pr.js';
|
|
14
14
|
import { pm2Logs } from './pm2-logs.js';
|
|
15
15
|
import { createBuilding } from './create-building.js';
|
|
16
|
+
import { exploreDatabase } from './explore-database.js';
|
|
16
17
|
import { releasePipeline } from './release-pipeline.js';
|
|
17
18
|
import { taskLabel } from './task-label.js';
|
|
18
19
|
import { agentTracking } from './agent-tracking.js';
|
|
@@ -34,6 +35,7 @@ export const BUILTIN_SKILLS = [
|
|
|
34
35
|
bitbucketPR,
|
|
35
36
|
pm2Logs,
|
|
36
37
|
createBuilding,
|
|
38
|
+
exploreDatabase,
|
|
37
39
|
releasePipeline,
|
|
38
40
|
taskLabel,
|
|
39
41
|
agentTracking,
|
|
@@ -323,6 +323,58 @@ export function getClaudeProjectDir(cwd) {
|
|
|
323
323
|
// Buildings cache to avoid reading from disk on every call
|
|
324
324
|
let buildingsCache = null;
|
|
325
325
|
let buildingsCacheMtime = 0;
|
|
326
|
+
// Sensitive fields on a DatabaseConnection that should be encrypted at rest.
|
|
327
|
+
// Kept as a typed list so adding a new sensitive field surfaces a compile error
|
|
328
|
+
// instead of silently leaking plaintext.
|
|
329
|
+
const DB_CONNECTION_SECRET_KEYS = ['password'];
|
|
330
|
+
const SSH_TUNNEL_SECRET_KEYS = ['password', 'privateKey', 'passphrase'];
|
|
331
|
+
function encryptIfPresent(value) {
|
|
332
|
+
if (!value)
|
|
333
|
+
return value;
|
|
334
|
+
if (isEncrypted(value))
|
|
335
|
+
return value;
|
|
336
|
+
try {
|
|
337
|
+
return encryptValue(value);
|
|
338
|
+
}
|
|
339
|
+
catch {
|
|
340
|
+
return value;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
function decryptIfEncrypted(value) {
|
|
344
|
+
if (!value)
|
|
345
|
+
return value;
|
|
346
|
+
if (!isEncrypted(value))
|
|
347
|
+
return value;
|
|
348
|
+
try {
|
|
349
|
+
return decryptValue(value);
|
|
350
|
+
}
|
|
351
|
+
catch (err) {
|
|
352
|
+
log.error(' Failed to decrypt building credential:', err);
|
|
353
|
+
return '';
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
function transformBuildingCredentials(buildings, mode) {
|
|
357
|
+
const transform = mode === 'encrypt' ? encryptIfPresent : decryptIfEncrypted;
|
|
358
|
+
return buildings.map(b => {
|
|
359
|
+
if (b.type !== 'database' || !b.database?.connections?.length)
|
|
360
|
+
return b;
|
|
361
|
+
const connections = b.database.connections.map(conn => {
|
|
362
|
+
const next = { ...conn };
|
|
363
|
+
for (const key of DB_CONNECTION_SECRET_KEYS) {
|
|
364
|
+
next[key] = transform(next[key]);
|
|
365
|
+
}
|
|
366
|
+
if (next.ssh) {
|
|
367
|
+
const ssh = { ...next.ssh };
|
|
368
|
+
for (const key of SSH_TUNNEL_SECRET_KEYS) {
|
|
369
|
+
ssh[key] = transform(ssh[key]);
|
|
370
|
+
}
|
|
371
|
+
next.ssh = ssh;
|
|
372
|
+
}
|
|
373
|
+
return next;
|
|
374
|
+
});
|
|
375
|
+
return { ...b, database: { ...b.database, connections } };
|
|
376
|
+
});
|
|
377
|
+
}
|
|
326
378
|
/**
|
|
327
379
|
* Load buildings from disk (cached, invalidated by file mtime)
|
|
328
380
|
*/
|
|
@@ -340,7 +392,7 @@ export function loadBuildings() {
|
|
|
340
392
|
catch { /* proceed to read */ }
|
|
341
393
|
const data = safeReadJsonSync(BUILDINGS_FILE, 'Buildings');
|
|
342
394
|
if (data?.buildings) {
|
|
343
|
-
buildingsCache = data.buildings;
|
|
395
|
+
buildingsCache = transformBuildingCredentials(data.buildings, 'decrypt');
|
|
344
396
|
try {
|
|
345
397
|
buildingsCacheMtime = fs.statSync(BUILDINGS_FILE).mtimeMs;
|
|
346
398
|
}
|
|
@@ -358,7 +410,9 @@ export function loadBuildings() {
|
|
|
358
410
|
export function saveBuildings(buildings) {
|
|
359
411
|
ensureDataDir();
|
|
360
412
|
try {
|
|
361
|
-
|
|
413
|
+
const persisted = transformBuildingCredentials(buildings, 'encrypt');
|
|
414
|
+
atomicWriteJsonSync(BUILDINGS_FILE, { buildings: persisted, savedAt: Date.now(), version: '1.0.0' });
|
|
415
|
+
// Cache the in-memory (decrypted) view so callers keep working with plaintext
|
|
362
416
|
buildingsCache = buildings;
|
|
363
417
|
buildingsCacheMtime = fs.statSync(BUILDINGS_FILE).mtimeMs;
|
|
364
418
|
}
|
|
@@ -7,7 +7,7 @@ import { createServer } from 'http';
|
|
|
7
7
|
import { createServer as createHttpsServer } from 'https';
|
|
8
8
|
import fs from 'node:fs';
|
|
9
9
|
import { createApp } from './app.js';
|
|
10
|
-
import { agentService, runtimeService, bossService, skillService, customClassService, secretsService, buildingService, eventRetentionService, triggerService, workflowService } from './services/index.js';
|
|
10
|
+
import { agentService, runtimeService, bossService, skillService, customClassService, secretsService, buildingService, eventRetentionService, triggerService, workflowService, databaseService } from './services/index.js';
|
|
11
11
|
import * as websocket from './websocket/handler.js';
|
|
12
12
|
import { getDataDir } from './data/index.js';
|
|
13
13
|
import { initEventDb, closeEventDb } from './data/event-db.js';
|
|
@@ -212,6 +212,7 @@ async function main() {
|
|
|
212
212
|
buildingService.stopDockerStatusPolling();
|
|
213
213
|
buildingService.stopTerminalStatusPolling();
|
|
214
214
|
buildingService.cleanupAllTerminals();
|
|
215
|
+
await databaseService.closeAllConnections();
|
|
215
216
|
await runtimeService.shutdown();
|
|
216
217
|
agentService.shutdownSessionHistory();
|
|
217
218
|
agentService.flushPersistAgents();
|
|
@@ -47,6 +47,10 @@ export const gmailTriggerHandler = {
|
|
|
47
47
|
extractVariables(trigger, event) {
|
|
48
48
|
const msg = event.data;
|
|
49
49
|
void trigger;
|
|
50
|
+
const labels = msg.labels ?? [];
|
|
51
|
+
// Gmail tags sent messages with the SENT label. Anything else is treated
|
|
52
|
+
// as inbound (covers INBOX, drafts, all-mail, etc.).
|
|
53
|
+
const direction = labels.includes('SENT') ? 'outbound' : 'inbound';
|
|
50
54
|
return {
|
|
51
55
|
'email.from': msg.from,
|
|
52
56
|
'email.to': msg.to.join(', '),
|
|
@@ -57,10 +61,14 @@ export const gmailTriggerHandler = {
|
|
|
57
61
|
'email.date': new Date(msg.date).toISOString(),
|
|
58
62
|
'email.hasAttachments': String(msg.hasAttachments),
|
|
59
63
|
'email.attachments': msg.attachmentNames?.join(', ') || '',
|
|
64
|
+
'email.direction': direction,
|
|
65
|
+
'email.labels': labels.join(', '),
|
|
60
66
|
};
|
|
61
67
|
},
|
|
62
68
|
formatEventForLLM(event) {
|
|
63
69
|
const msg = event.data;
|
|
64
|
-
|
|
70
|
+
const labels = msg.labels ?? [];
|
|
71
|
+
const direction = labels.includes('SENT') ? 'outbound' : 'inbound';
|
|
72
|
+
return `Email (${direction}) from ${msg.from}\nTo: ${msg.to.join(', ')}\nSubject: ${msg.subject}\nDate: ${new Date(msg.date).toISOString()}\n\n${msg.body}`;
|
|
65
73
|
},
|
|
66
74
|
};
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import { createWhatsAppRoutes } from './whatsapp-routes.js';
|
|
12
12
|
import { whatsappConfigSchema, getConfigValues, setConfigValues, loadConfig, WHATSAPP_API_KEY_SECRET, } from './whatsapp-config.js';
|
|
13
|
-
import { createWhatsAppTriggerHandler, } from './whatsapp-trigger-handler.js';
|
|
13
|
+
import { createWhatsAppTriggerHandler, whatsappTriggerHandler, } from './whatsapp-trigger-handler.js';
|
|
14
14
|
import { whatsappSkill } from './whatsapp-skill.js';
|
|
15
15
|
let integrationCtx = null;
|
|
16
16
|
let lastChecked = 0;
|
|
@@ -99,9 +99,10 @@ export const whatsappPlugin = {
|
|
|
99
99
|
return [whatsappSkill];
|
|
100
100
|
},
|
|
101
101
|
getTriggerHandler() {
|
|
102
|
-
//
|
|
103
|
-
//
|
|
104
|
-
|
|
102
|
+
// Real trigger-service handler. Subscribes to the in-process bridge's
|
|
103
|
+
// notifyTriggerSubscribers stream so events flow into trigger evaluation
|
|
104
|
+
// even across bridge restarts.
|
|
105
|
+
return whatsappTriggerHandler;
|
|
105
106
|
},
|
|
106
107
|
getStatus() {
|
|
107
108
|
const config = loadConfig();
|
|
@@ -39,6 +39,16 @@ export class WhatsAppClient {
|
|
|
39
39
|
const data = await this.request('GET', `/api/sessions/${encodeURIComponent(sessionId)}/contacts`);
|
|
40
40
|
return Array.isArray(data) ? data : [];
|
|
41
41
|
}
|
|
42
|
+
async syncChatMessages(sessionId, chatId, count = 50) {
|
|
43
|
+
return this.request('POST', `/api/sessions/${encodeURIComponent(sessionId)}/chats/${encodeURIComponent(chatId)}/sync-messages?count=${encodeURIComponent(String(count))}`, {});
|
|
44
|
+
}
|
|
45
|
+
async syncContacts(sessionId) {
|
|
46
|
+
return this.request('POST', `/api/sessions/${encodeURIComponent(sessionId)}/sync-contacts`, {});
|
|
47
|
+
}
|
|
48
|
+
async getChatMessages(sessionId, chatId, limit = 50) {
|
|
49
|
+
const data = await this.request('GET', `/api/sessions/${encodeURIComponent(sessionId)}/chats/${encodeURIComponent(chatId)}/messages?limit=${encodeURIComponent(String(limit))}`);
|
|
50
|
+
return Array.isArray(data) ? data : [];
|
|
51
|
+
}
|
|
42
52
|
async sendMediaUrl(sessionId, to, mediaUrl, caption, options) {
|
|
43
53
|
// Upstream body shape: { to, url, caption?, filename? } (type is auto-detected
|
|
44
54
|
// from the fetched URL's Content-Type header). `type` is forwarded for
|
|
@@ -198,6 +198,74 @@ export function createWhatsAppRoutes(ctx) {
|
|
|
198
198
|
res.status(502).json({ error: err instanceof Error ? err.message : String(err) });
|
|
199
199
|
}
|
|
200
200
|
});
|
|
201
|
+
// ─── POST /sessions/:sessionId/chats/:chatId/sync-messages — Force on-demand history sync for a chat ───
|
|
202
|
+
router.post('/sessions/:sessionId/chats/:chatId/sync-messages', async (req, res) => {
|
|
203
|
+
const built = getClient();
|
|
204
|
+
if ('error' in built) {
|
|
205
|
+
res.status(built.status).json({ error: built.error });
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
const rawCount = typeof req.query.count === 'string' ? Number(req.query.count) : NaN;
|
|
209
|
+
const count = Number.isFinite(rawCount) && rawCount > 0 ? Math.floor(rawCount) : 50;
|
|
210
|
+
try {
|
|
211
|
+
const result = await built.client.syncChatMessages(req.params.sessionId, req.params.chatId, count);
|
|
212
|
+
res.json({ success: true, data: result });
|
|
213
|
+
}
|
|
214
|
+
catch (err) {
|
|
215
|
+
log.error(`WhatsApp syncChatMessages error: ${err}`);
|
|
216
|
+
res.status(502).json({ error: err instanceof Error ? err.message : String(err) });
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
// ─── POST /sessions/:sessionId/sync-contacts — Force address-book resync via Baileys app-state ───
|
|
220
|
+
router.post('/sessions/:sessionId/sync-contacts', async (req, res) => {
|
|
221
|
+
const built = getClient();
|
|
222
|
+
if ('error' in built) {
|
|
223
|
+
res.status(built.status).json({ error: built.error });
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
try {
|
|
227
|
+
const result = await built.client.syncContacts(req.params.sessionId);
|
|
228
|
+
res.json({ success: true, data: result });
|
|
229
|
+
}
|
|
230
|
+
catch (err) {
|
|
231
|
+
log.error(`WhatsApp syncContacts error: ${err}`);
|
|
232
|
+
res.status(502).json({ error: err instanceof Error ? err.message : String(err) });
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
// ─── GET /sessions/:sessionId/contacts — List contacts for a session ───
|
|
236
|
+
router.get('/sessions/:sessionId/contacts', async (req, res) => {
|
|
237
|
+
const built = getClient();
|
|
238
|
+
if ('error' in built) {
|
|
239
|
+
res.status(built.status).json({ error: built.error });
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
try {
|
|
243
|
+
const result = await built.client.getContacts(req.params.sessionId);
|
|
244
|
+
res.json({ success: true, data: result });
|
|
245
|
+
}
|
|
246
|
+
catch (err) {
|
|
247
|
+
log.error(`WhatsApp getContacts error: ${err}`);
|
|
248
|
+
res.status(502).json({ error: err instanceof Error ? err.message : String(err) });
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
// ─── GET /sessions/:sessionId/chats/:chatId/messages — Fetch recent messages for a chat ───
|
|
252
|
+
router.get('/sessions/:sessionId/chats/:chatId/messages', async (req, res) => {
|
|
253
|
+
const built = getClient();
|
|
254
|
+
if ('error' in built) {
|
|
255
|
+
res.status(built.status).json({ error: built.error });
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
const rawLimit = typeof req.query.limit === 'string' ? Number(req.query.limit) : NaN;
|
|
259
|
+
const limit = Number.isFinite(rawLimit) && rawLimit > 0 ? Math.floor(rawLimit) : 50;
|
|
260
|
+
try {
|
|
261
|
+
const result = await built.client.getChatMessages(req.params.sessionId, req.params.chatId, limit);
|
|
262
|
+
res.json({ success: true, data: result });
|
|
263
|
+
}
|
|
264
|
+
catch (err) {
|
|
265
|
+
log.error(`WhatsApp getChatMessages error: ${err}`);
|
|
266
|
+
res.status(502).json({ error: err instanceof Error ? err.message : String(err) });
|
|
267
|
+
}
|
|
268
|
+
});
|
|
201
269
|
// ─── POST /send-message — Send a text message via Baileys ───
|
|
202
270
|
router.post('/send-message', async (req, res) => {
|
|
203
271
|
const body = (req.body ?? {});
|
|
@@ -13,6 +13,21 @@ import { WhatsAppWsClient } from './whatsapp-ws-client.js';
|
|
|
13
13
|
import { MessageDedupeCache } from './message-dedupe.js';
|
|
14
14
|
import { ContactNameCache } from './contact-name-cache.js';
|
|
15
15
|
import { WhatsAppClient } from './whatsapp-client.js';
|
|
16
|
+
// Module-level subscriber set fed by the in-process bridge whenever a normalized
|
|
17
|
+
// WhatsApp message is broadcast. The trigger-service-facing handler below
|
|
18
|
+
// registers its callback here, so subscribers stay attached even when the bridge
|
|
19
|
+
// restarts (e.g. on config change).
|
|
20
|
+
const triggerSubscribers = new Set();
|
|
21
|
+
function notifyTriggerSubscribers(payload) {
|
|
22
|
+
for (const cb of triggerSubscribers) {
|
|
23
|
+
try {
|
|
24
|
+
cb(payload);
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
// Subscriber errors must not break the bridge — swallow.
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
16
31
|
// Dedupe cache config. The upstream whatsapp-api fires BOTH `message` and
|
|
17
32
|
// `message_create` for the same Baileys event on inbound DMs (see
|
|
18
33
|
// providers/baileysSessionManager.js around the `messages.upsert` handler — it
|
|
@@ -105,6 +120,7 @@ export function createWhatsAppTriggerHandler(ctx) {
|
|
|
105
120
|
!!payload.from;
|
|
106
121
|
if (!needsEnrichment) {
|
|
107
122
|
ctx.broadcast({ type: 'whatsapp_message', payload });
|
|
123
|
+
notifyTriggerSubscribers(payload);
|
|
108
124
|
return;
|
|
109
125
|
}
|
|
110
126
|
void contactNames.lookup(msg.sessionId, payload.from).then((name) => {
|
|
@@ -112,9 +128,11 @@ export function createWhatsAppTriggerHandler(ctx) {
|
|
|
112
128
|
if (sanitized)
|
|
113
129
|
payload.fromName = sanitized;
|
|
114
130
|
ctx.broadcast({ type: 'whatsapp_message', payload });
|
|
131
|
+
notifyTriggerSubscribers(payload);
|
|
115
132
|
}, (err) => {
|
|
116
133
|
ctx.log.warn(`WhatsApp contact lookup failed: ${err}`);
|
|
117
134
|
ctx.broadcast({ type: 'whatsapp_message', payload });
|
|
135
|
+
notifyTriggerSubscribers(payload);
|
|
118
136
|
});
|
|
119
137
|
}
|
|
120
138
|
// Other event types (message_ack, group_join, group_leave, hello, error) are
|
|
@@ -216,6 +234,7 @@ function normalizeBaileysMessage(sessionId, eventName, data, ts, baseUrl) {
|
|
|
216
234
|
mediaType,
|
|
217
235
|
mediaUrl,
|
|
218
236
|
direction,
|
|
237
|
+
chatId: remoteJid ?? from,
|
|
219
238
|
};
|
|
220
239
|
}
|
|
221
240
|
function extractContent(envelope, message) {
|
|
@@ -355,3 +374,97 @@ export function sanitizeFromName(name, fromJid) {
|
|
|
355
374
|
}
|
|
356
375
|
return trimmed;
|
|
357
376
|
}
|
|
377
|
+
// Chat IDs that represent non-message channels (status updates, broadcast
|
|
378
|
+
// lists). The bridge surfaces them as messages because Baileys delivers
|
|
379
|
+
// status posts via `messages.upsert`, but they're not 1:1 / group chats and
|
|
380
|
+
// most triggers shouldn't fire on them.
|
|
381
|
+
function isStatusOrBroadcastChat(chatId) {
|
|
382
|
+
if (!chatId)
|
|
383
|
+
return false;
|
|
384
|
+
if (chatId === 'status@broadcast')
|
|
385
|
+
return true;
|
|
386
|
+
if (chatId.endsWith('@broadcast'))
|
|
387
|
+
return true;
|
|
388
|
+
return false;
|
|
389
|
+
}
|
|
390
|
+
let triggerUnsubscribe = null;
|
|
391
|
+
export const whatsappTriggerHandler = {
|
|
392
|
+
triggerType: 'whatsapp',
|
|
393
|
+
async startListening(onEvent) {
|
|
394
|
+
const cb = (msg) => {
|
|
395
|
+
onEvent({
|
|
396
|
+
source: 'whatsapp',
|
|
397
|
+
type: 'message',
|
|
398
|
+
data: msg,
|
|
399
|
+
timestamp: msg.timestamp,
|
|
400
|
+
});
|
|
401
|
+
};
|
|
402
|
+
triggerSubscribers.add(cb);
|
|
403
|
+
triggerUnsubscribe = () => triggerSubscribers.delete(cb);
|
|
404
|
+
},
|
|
405
|
+
async stopListening() {
|
|
406
|
+
if (triggerUnsubscribe) {
|
|
407
|
+
triggerUnsubscribe();
|
|
408
|
+
triggerUnsubscribe = null;
|
|
409
|
+
}
|
|
410
|
+
},
|
|
411
|
+
structuralMatch(trigger, event) {
|
|
412
|
+
const msg = event.data;
|
|
413
|
+
const cfg = trigger.config;
|
|
414
|
+
// Drop status updates and broadcast-list messages by default — they're
|
|
415
|
+
// surfaced through the same bridge as real messages but represent stories /
|
|
416
|
+
// broadcasts, not conversations. Opt-in via includeStatuses.
|
|
417
|
+
if (!cfg.includeStatuses && isStatusOrBroadcastChat(msg.chatId))
|
|
418
|
+
return false;
|
|
419
|
+
if (cfg.direction && cfg.direction !== 'any' && msg.direction !== cfg.direction)
|
|
420
|
+
return false;
|
|
421
|
+
if (cfg.groupOnly && !msg.isGroup)
|
|
422
|
+
return false;
|
|
423
|
+
if (cfg.dmOnly && msg.isGroup)
|
|
424
|
+
return false;
|
|
425
|
+
if (cfg.sessionId && msg.sessionId !== cfg.sessionId)
|
|
426
|
+
return false;
|
|
427
|
+
if (cfg.fromFilter?.length) {
|
|
428
|
+
const fromLower = msg.from.toLowerCase();
|
|
429
|
+
if (!cfg.fromFilter.some(f => fromLower.includes(f.toLowerCase())))
|
|
430
|
+
return false;
|
|
431
|
+
}
|
|
432
|
+
if (cfg.bodyPattern) {
|
|
433
|
+
try {
|
|
434
|
+
if (!new RegExp(cfg.bodyPattern, 'i').test(msg.body))
|
|
435
|
+
return false;
|
|
436
|
+
}
|
|
437
|
+
catch {
|
|
438
|
+
return false;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
return true;
|
|
442
|
+
},
|
|
443
|
+
extractVariables(trigger, event) {
|
|
444
|
+
const msg = event.data;
|
|
445
|
+
void trigger;
|
|
446
|
+
return {
|
|
447
|
+
'whatsapp.from': msg.from,
|
|
448
|
+
'whatsapp.fromName': msg.fromName ?? '',
|
|
449
|
+
'whatsapp.body': msg.body,
|
|
450
|
+
'whatsapp.sessionId': msg.sessionId,
|
|
451
|
+
'whatsapp.chatId': msg.chatId,
|
|
452
|
+
'whatsapp.isGroup': String(msg.isGroup),
|
|
453
|
+
'whatsapp.groupName': msg.groupName ?? '',
|
|
454
|
+
'whatsapp.direction': msg.direction,
|
|
455
|
+
'whatsapp.mediaType': msg.mediaType ?? '',
|
|
456
|
+
'whatsapp.mediaUrl': msg.mediaUrl ?? '',
|
|
457
|
+
'whatsapp.timestamp': new Date(msg.timestamp).toISOString(),
|
|
458
|
+
};
|
|
459
|
+
},
|
|
460
|
+
formatEventForLLM(event) {
|
|
461
|
+
const msg = event.data;
|
|
462
|
+
const sender = msg.fromName ? `${msg.fromName} (${msg.from})` : msg.from;
|
|
463
|
+
const channel = msg.isGroup ? `group "${msg.groupName ?? msg.from}"` : 'DM';
|
|
464
|
+
const verb = msg.direction === 'outbound' ? 'sent' : 'received';
|
|
465
|
+
const media = msg.mediaType
|
|
466
|
+
? `\n[${msg.mediaType} attachment${msg.mediaUrl ? ` ${msg.mediaUrl}` : ''}]`
|
|
467
|
+
: '';
|
|
468
|
+
return `WhatsApp message ${verb} in ${channel}\nFrom: ${sender}\nSession: ${msg.sessionId}\nTime: ${new Date(msg.timestamp).toISOString()}${media}\n\n${msg.body}`;
|
|
469
|
+
},
|
|
470
|
+
};
|