tide-commander 1.87.0 → 1.89.0
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-BK6N5fG2.js} +1 -1
- package/dist/assets/{BossSpawnModal-BjWGNCnz.js → BossSpawnModal-BTy-lus4.js} +1 -1
- package/dist/assets/{ControlsModal-6yfU0XjZ.js → ControlsModal-B4MhaF1V.js} +1 -1
- package/dist/assets/{DockerLogsModal-CYq0hNz6.js → DockerLogsModal-C33dAwy1.js} +1 -1
- package/dist/assets/{EmbeddedEditor-ZBdqRDqm.js → EmbeddedEditor-BfjjT-GF.js} +1 -1
- package/dist/assets/{GmailOAuthSetup-BcV5jAse.js → GmailOAuthSetup-TQyjHs3_.js} +1 -1
- package/dist/assets/{GoogleOAuthSetup-DyUW_STE.js → GoogleOAuthSetup-DAIzYKy8.js} +1 -1
- package/dist/assets/{IframeModal-D9A3dUUc.js → IframeModal-g8tC4aah.js} +1 -1
- package/dist/assets/IntegrationsPanel-CuKr7702.js +2 -0
- package/dist/assets/{LogViewerModal-BWkbY7wa.js → LogViewerModal-DO45Kea0.js} +1 -1
- package/dist/assets/{MonitoringModal-AZzokZAZ.js → MonitoringModal-OIwmagj2.js} +1 -1
- package/dist/assets/{PM2LogsModal-q98eiBfq.js → PM2LogsModal-BRQzSiFN.js} +1 -1
- package/dist/assets/{RestoreArchivedAreaModal-CTxRP2qE.js → RestoreArchivedAreaModal-CBRN9Xpb.js} +1 -1
- package/dist/assets/{Scene2DCanvas-C11dztp1.js → Scene2DCanvas-4J4ZefT6.js} +1 -1
- package/dist/assets/{SceneManager-CsW9MYrD.js → SceneManager-DZsJcYvW.js} +1 -1
- package/dist/assets/{SkillsPanel-BeZr9w6E.js → SkillsPanel-DHk7h3Ja.js} +1 -1
- package/dist/assets/SlackMultiInstanceSetup-Dp1q2zM1.js +2 -0
- package/dist/assets/{SpawnModal-DY_KM6lX.js → SpawnModal-CfozYMNI.js} +1 -1
- package/dist/assets/{SubordinateAssignmentModal-D6RvjGX9.js → SubordinateAssignmentModal-BBfbpVUr.js} +1 -1
- package/dist/assets/{TriggerManagerPanel-BmqjXv9T.js → TriggerManagerPanel-DQw9nt1r.js} +2 -2
- package/dist/assets/{WorkflowEditorPanel-Rd5ZjJmt.js → WorkflowEditorPanel-BM2ec8CS.js} +1 -1
- package/dist/assets/{index-DSvJOrb7.js → index-BiAZinYH.js} +2 -2
- package/dist/assets/{index-BYVHgVEo.js → index-BqbR55dr.js} +1 -1
- package/dist/assets/{index-DRGyDtmm.js → index-CcSJA57k.js} +1 -1
- package/dist/assets/{index-BtJyOo4p.js → index-DNEUJDeO.js} +1 -1
- package/dist/assets/{index-BoORE9Q1.js → index-DY9w7IcH.js} +1 -1
- package/dist/assets/{index-DHHRkTG1.js → index-bcwTXJ6F.js} +1 -1
- package/dist/assets/index-fZfyvIUZ.js +2 -0
- package/dist/assets/{index-BxaEkSIx.js → index-jXkaBxIq.js} +3 -3
- package/dist/assets/index-xEvpFBA8.js +8 -0
- package/dist/assets/main-Bw5ZddEN.css +1 -0
- package/dist/assets/main-D-YFCprA.js +213 -0
- package/dist/assets/{web-D3zCwsS9.js → web-BrBkKQlr.js} +1 -1
- package/dist/assets/{web-DS0FHmg8.js → web-DCu3NTho.js} +1 -1
- package/dist/assets/{web-DEq3Te_H.js → web-DX588C-g.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/event-queries.js +2 -0
- package/dist/src/packages/server/data/index.js +56 -2
- package/dist/src/packages/server/data/migrations/006_slack_messages_integration_instance.sql +9 -0
- 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/slack/index.js +65 -19
- package/dist/src/packages/server/integrations/slack/slack-client.js +44 -602
- package/dist/src/packages/server/integrations/slack/slack-config.js +229 -29
- package/dist/src/packages/server/integrations/slack/slack-instance-manifest.js +150 -0
- package/dist/src/packages/server/integrations/slack/slack-instance.js +801 -0
- package/dist/src/packages/server/integrations/slack/slack-polling-client.js +522 -0
- package/dist/src/packages/server/integrations/slack/slack-routes.js +243 -24
- package/dist/src/packages/server/integrations/slack/slack-trigger-handler.js +53 -20
- package/dist/src/packages/server/integrations/slack/slack-watermark-store.js +124 -0
- 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 +127 -0
- package/dist/src/packages/server/routes/database.js +221 -0
- package/dist/src/packages/server/routes/files.js +219 -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/IntegrationsPanel-CHaNJBJW.js +0 -2
- 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
- package/dist/assets/main-kpU9m5LW.css +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
import{ck as s}from"./main-
|
|
1
|
+
import{ck as s}from"./main-D-YFCprA.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 a}from"./main-
|
|
1
|
+
import{ck as a}from"./main-D-YFCprA.js";import{ImpactStyle as i,NotificationType as r}from"./index-BiAZinYH.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 t}from"./main-
|
|
1
|
+
import{ck as t}from"./main-D-YFCprA.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-D-YFCprA.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-Bw5ZddEN.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,
|
|
@@ -75,6 +75,7 @@ function slackRowToEvent(row) {
|
|
|
75
75
|
workflowInstanceId: row.workflow_instance_id ?? undefined,
|
|
76
76
|
rawEvent: fromJson(row.raw_event),
|
|
77
77
|
receivedAt: row.received_at,
|
|
78
|
+
integrationInstanceId: row.integration_instance_id ?? 'default',
|
|
78
79
|
};
|
|
79
80
|
}
|
|
80
81
|
export function logSlackMessage(msg) {
|
|
@@ -91,6 +92,7 @@ export function logSlackMessage(msg) {
|
|
|
91
92
|
workflow_instance_id: msg.workflowInstanceId ?? null,
|
|
92
93
|
raw_event: toJson(msg.rawEvent),
|
|
93
94
|
received_at: msg.receivedAt,
|
|
95
|
+
integration_instance_id: msg.integrationInstanceId ?? 'default',
|
|
94
96
|
});
|
|
95
97
|
}
|
|
96
98
|
function emailRowToEvent(row) {
|
|
@@ -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
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
-- Multi-instance Slack support
|
|
2
|
+
-- Adds an instance id column to slack_messages so two side-by-side Slack
|
|
3
|
+
-- integrations (e.g. xoxb- bot + xoxp- personal token) keep their inbound
|
|
4
|
+
-- messages distinguishable. Existing rows are tagged 'default' to match the
|
|
5
|
+
-- pre-multi-instance singleton.
|
|
6
|
+
|
|
7
|
+
ALTER TABLE slack_messages ADD COLUMN integration_instance_id TEXT NOT NULL DEFAULT 'default';
|
|
8
|
+
|
|
9
|
+
CREATE INDEX IF NOT EXISTS idx_slack_messages_instance ON slack_messages(integration_instance_id);
|
|
@@ -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
|
};
|
|
@@ -1,24 +1,52 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Slack Integration Plugin
|
|
3
3
|
* Exports slackPlugin implementing IntegrationPlugin.
|
|
4
|
-
*
|
|
4
|
+
*
|
|
5
|
+
* Multi-instance: this plugin owns N independent Slack connections (e.g. one
|
|
6
|
+
* for a team bot using xoxb-, one for the user's personal xoxp- token). Each
|
|
7
|
+
* instance has its own config file, secret keys, and watermark file, but they
|
|
8
|
+
* share the same dispatcher pipeline (so triggers / wait-for-reply / WS
|
|
9
|
+
* broadcasts work uniformly).
|
|
10
|
+
*
|
|
11
|
+
* The IntegrationPlugin contract is per-plugin, not per-instance — Slack
|
|
12
|
+
* exposes its instance management through a custom settings component
|
|
13
|
+
* (registered via `getCustomSettingsComponent()`) and dedicated /instances
|
|
14
|
+
* CRUD routes.
|
|
5
15
|
*/
|
|
6
16
|
import * as slackClient from './slack-client.js';
|
|
7
|
-
import slackRoutes from './slack-routes.js';
|
|
17
|
+
import slackRoutes, { setIntegrationContextForRoutes } from './slack-routes.js';
|
|
8
18
|
import { slackTriggerHandler } from './slack-trigger-handler.js';
|
|
9
19
|
import { slackSkill } from './slack-skill.js';
|
|
10
|
-
import { slackConfigSchema, getConfigValues, setConfigValues, loadConfig } from './slack-config.js';
|
|
20
|
+
import { slackConfigSchema, getConfigValues, setConfigValues, loadConfig, } from './slack-config.js';
|
|
21
|
+
import { getInstance, listInstances, DEFAULT_INSTANCE_ID, } from './slack-instance.js';
|
|
22
|
+
import { listInstanceMetas } from './slack-instance-manifest.js';
|
|
11
23
|
let integrationCtx = null;
|
|
12
24
|
export const slackPlugin = {
|
|
13
25
|
id: 'slack',
|
|
14
26
|
name: 'Slack',
|
|
15
|
-
description: 'Bidirectional Slack messaging for agents',
|
|
27
|
+
description: 'Bidirectional Slack messaging for agents (multi-instance)',
|
|
16
28
|
routePrefix: '/slack',
|
|
17
29
|
async init(ctx) {
|
|
18
30
|
integrationCtx = ctx;
|
|
19
|
-
|
|
31
|
+
setIntegrationContextForRoutes(ctx);
|
|
32
|
+
// Bring up every instance from the manifest. The default instance is
|
|
33
|
+
// always present (the manifest auto-creates it); user-added instances
|
|
34
|
+
// come in alongside it.
|
|
35
|
+
const metas = listInstanceMetas();
|
|
36
|
+
for (const meta of metas) {
|
|
37
|
+
const inst = getInstance(meta.id);
|
|
38
|
+
inst.setContext(ctx);
|
|
39
|
+
try {
|
|
40
|
+
await inst.init();
|
|
41
|
+
}
|
|
42
|
+
catch (err) {
|
|
43
|
+
ctx.log.error(`Slack[${meta.id}] init failed: ${err}`);
|
|
44
|
+
// One bad instance shouldn't take the others down.
|
|
45
|
+
}
|
|
46
|
+
}
|
|
20
47
|
},
|
|
21
48
|
async shutdown() {
|
|
49
|
+
setIntegrationContextForRoutes(null);
|
|
22
50
|
await slackClient.shutdown();
|
|
23
51
|
},
|
|
24
52
|
getRoutes() {
|
|
@@ -30,15 +58,25 @@ export const slackPlugin = {
|
|
|
30
58
|
getTriggerHandler() {
|
|
31
59
|
return slackTriggerHandler;
|
|
32
60
|
},
|
|
61
|
+
/**
|
|
62
|
+
* Status reflects the default instance (back-compat for the generic UI).
|
|
63
|
+
* Per-instance status is exposed through GET /api/slack/instances and the
|
|
64
|
+
* custom settings component reads from there.
|
|
65
|
+
*/
|
|
33
66
|
getStatus() {
|
|
34
|
-
return
|
|
67
|
+
return getInstance(DEFAULT_INSTANCE_ID).getStatus();
|
|
35
68
|
},
|
|
36
69
|
getConfigSchema() {
|
|
37
70
|
return slackConfigSchema;
|
|
38
71
|
},
|
|
72
|
+
/**
|
|
73
|
+
* The generic UI calls getConfig once and gets the default-instance values.
|
|
74
|
+
* The custom multi-instance UI uses /api/slack/instances/:id for per-instance
|
|
75
|
+
* config.
|
|
76
|
+
*/
|
|
39
77
|
getConfig() {
|
|
40
78
|
if (!integrationCtx) {
|
|
41
|
-
const config = loadConfig();
|
|
79
|
+
const config = loadConfig(DEFAULT_INSTANCE_ID);
|
|
42
80
|
return {
|
|
43
81
|
enabled: config.enabled,
|
|
44
82
|
defaultChannelId: config.defaultChannelId || '',
|
|
@@ -46,28 +84,36 @@ export const slackPlugin = {
|
|
|
46
84
|
SLACK_APP_TOKEN: '',
|
|
47
85
|
};
|
|
48
86
|
}
|
|
49
|
-
return getConfigValues(integrationCtx.secrets);
|
|
87
|
+
return getConfigValues(integrationCtx.secrets, DEFAULT_INSTANCE_ID);
|
|
50
88
|
},
|
|
51
89
|
async setConfig(config) {
|
|
52
90
|
if (!integrationCtx)
|
|
53
91
|
throw new Error('Slack not initialized');
|
|
54
|
-
await setConfigValues(config, integrationCtx.secrets);
|
|
55
|
-
//
|
|
56
|
-
const updated = loadConfig();
|
|
92
|
+
await setConfigValues(config, integrationCtx.secrets, DEFAULT_INSTANCE_ID);
|
|
93
|
+
// Reconnect the default instance based on its new config / secrets.
|
|
94
|
+
const updated = loadConfig(DEFAULT_INSTANCE_ID);
|
|
57
95
|
const botToken = integrationCtx.secrets.get('SLACK_BOT_TOKEN');
|
|
58
|
-
const
|
|
59
|
-
if (updated.enabled && botToken
|
|
60
|
-
// Tokens + enabled: (re)connect
|
|
96
|
+
const inst = getInstance(DEFAULT_INSTANCE_ID);
|
|
97
|
+
if (updated.enabled && botToken) {
|
|
61
98
|
try {
|
|
62
|
-
await
|
|
99
|
+
await inst.reconnect();
|
|
63
100
|
}
|
|
64
101
|
catch {
|
|
65
|
-
// Error
|
|
102
|
+
// Error captured via updateConfig's status:'error' path.
|
|
66
103
|
}
|
|
67
104
|
}
|
|
68
|
-
else if (!updated.enabled &&
|
|
69
|
-
|
|
70
|
-
await slackClient.disconnect();
|
|
105
|
+
else if (!updated.enabled && inst.isConnected()) {
|
|
106
|
+
await inst.disconnect();
|
|
71
107
|
}
|
|
72
108
|
},
|
|
109
|
+
/**
|
|
110
|
+
* Tells the frontend to render the custom multi-instance UI instead of the
|
|
111
|
+
* generic single-form schema renderer. The component name is mapped client-
|
|
112
|
+
* side in IntegrationsPanel.tsx.
|
|
113
|
+
*/
|
|
114
|
+
getCustomSettingsComponent() {
|
|
115
|
+
return 'slack-multi-instance';
|
|
116
|
+
},
|
|
73
117
|
};
|
|
118
|
+
// Re-export for use by routes / trigger handler.
|
|
119
|
+
export { listInstances };
|