tide-commander 1.97.0 → 1.99.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-CT25hD17.js → BossLogsModal-CDel834o.js} +1 -1
- package/dist/assets/{BossSpawnModal-9rS7AFkZ.js → BossSpawnModal-BB9wL5VV.js} +1 -1
- package/dist/assets/{ControlsModal-D-mymoM7.js → ControlsModal-D5RE5MvT.js} +1 -1
- package/dist/assets/{DockerLogsModal-Ae-ZCeeP.js → DockerLogsModal-B27P1JpZ.js} +1 -1
- package/dist/assets/{EmbeddedEditor-DLOOpM0K.js → EmbeddedEditor-DP1jqsT_.js} +1 -1
- package/dist/assets/{GmailOAuthSetup-C9NLhWLo.js → GmailOAuthSetup-DvuL5G8Q.js} +1 -1
- package/dist/assets/{GoogleOAuthSetup-1kzgrPV6.js → GoogleOAuthSetup-CG6bSCjv.js} +1 -1
- package/dist/assets/{IframeModal-DKS0IFsr.js → IframeModal-ClnUGmJV.js} +1 -1
- package/dist/assets/{IntegrationsPanel-CBvKOeud.js → IntegrationsPanel-0vdfxZRq.js} +2 -2
- package/dist/assets/{LogViewerModal-Dlt8JfVg.js → LogViewerModal-DLQlrZ4O.js} +1 -1
- package/dist/assets/{MonitoringModal-BM1IEZv6.js → MonitoringModal-DiC9TNCy.js} +1 -1
- package/dist/assets/{PM2LogsModal-B1-HUHWZ.js → PM2LogsModal-BgPrnaP5.js} +1 -1
- package/dist/assets/{RestoreArchivedAreaModal-DXmYo7fp.js → RestoreArchivedAreaModal-CIN1OrOW.js} +1 -1
- package/dist/assets/{Scene2DCanvas-CuUxSaPb.js → Scene2DCanvas-Bap6brvv.js} +1 -1
- package/dist/assets/{SceneManager-UD3IHY20.js → SceneManager-CidCW0PR.js} +1 -1
- package/dist/assets/{SkillsPanel-DjRBVrO2.js → SkillsPanel-DHQTPaP2.js} +1 -1
- package/dist/assets/{SlackMultiInstanceSetup-Csp81Dqn.js → SlackMultiInstanceSetup-CQK4D89W.js} +1 -1
- package/dist/assets/{SpawnModal-dg0mH3d9.js → SpawnModal-Cx_k3HHC.js} +1 -1
- package/dist/assets/{SubordinateAssignmentModal-CeBPRNNX.js → SubordinateAssignmentModal-C1DOv51H.js} +1 -1
- package/dist/assets/TriggerManagerPanel-jP5RBK2L.js +9 -0
- package/dist/assets/{WorkflowEditorPanel-IIsptZgp.js → WorkflowEditorPanel-Dh6mZ8M4.js} +1 -1
- package/dist/assets/{index-h-IcmGfB.js → index-B-ttQFx4.js} +2 -2
- package/dist/assets/index-BXnThzaG.js +11 -0
- package/dist/assets/index-CK8NcQSU.css +1 -0
- package/dist/assets/{index-CNDUxsGy.js → index-CYwFXTQZ.js} +1 -1
- package/dist/assets/{index-sDgBtEgH.js → index-Cwlm-Pqi.js} +3 -3
- package/dist/assets/index-D96LXKm4.js +1 -0
- package/dist/assets/{index-BGh9tRSy.js → index-DP5sMNS9.js} +1 -1
- package/dist/assets/{index-CsyPNc8u.js → index-DfEbuBH8.js} +1 -1
- package/dist/assets/{index-DEI-vrXk.js → index-DsKaX6TJ.js} +1 -1
- package/dist/assets/{index-CIqkVLo1.js → index-enJvXAbe.js} +1 -1
- package/dist/assets/main-B7wf_xU_.js +214 -0
- package/dist/assets/main-DLzFxLC1.css +1 -0
- package/dist/assets/{web-BmPSJLwQ.js → web-BHmmnvF7.js} +1 -1
- package/dist/assets/{web-Dggt4D4N.js → web-IGuhG0xr.js} +1 -1
- package/dist/assets/{web-BgPjNMBK.js → web-SOehUGgT.js} +1 -1
- package/dist/index.html +2 -2
- package/dist/locales/en/config.json +60 -1
- package/dist/locales/en/terminal.json +10 -0
- package/dist/src/packages/server/claude/backend.js +42 -0
- package/dist/src/packages/server/claude/permission-prompt-server.mjs +188 -0
- package/dist/src/packages/server/claude/runner/process-lifecycle.js +3 -0
- package/dist/src/packages/server/claude/runner/stdout-pipeline.js +8 -0
- package/dist/src/packages/server/claude/runner/tmux-helper.js +14 -0
- package/dist/src/packages/server/data/event-queries.js +143 -1
- package/dist/src/packages/server/data/migrations/007_whatsapp_messages.sql +48 -0
- package/dist/src/packages/server/index.js +1 -0
- package/dist/src/packages/server/integrations/gmail/gmail-client.js +139 -24
- package/dist/src/packages/server/integrations/gmail/gmail-routes.js +162 -0
- package/dist/src/packages/server/integrations/whatsapp/whatsapp-routes.js +81 -0
- package/dist/src/packages/server/integrations/whatsapp/whatsapp-trigger-handler.js +29 -0
- package/dist/src/packages/server/routes/agent-prompt.js +57 -0
- package/dist/src/packages/server/routes/index.js +8 -1
- package/dist/src/packages/server/routes/skills.js +193 -0
- package/dist/src/packages/server/routes/system.js +156 -0
- package/dist/src/packages/server/routes/trigger-routes.js +74 -17
- package/dist/src/packages/server/routes/webhook-signatures.js +20 -7
- package/dist/src/packages/server/services/agent-prompt-service.js +100 -0
- package/dist/src/packages/server/services/index.js +1 -0
- package/dist/src/packages/server/services/self-update-service.js +191 -0
- package/dist/src/packages/server/websocket/handler.js +2 -1
- package/dist/src/packages/server/websocket/listeners/agent-prompt-listeners.js +13 -0
- package/dist/src/packages/server/websocket/listeners/index.js +2 -0
- package/dist/src/packages/shared/whatsapp-types.js +1 -0
- package/package.json +2 -2
- package/dist/assets/TriggerManagerPanel-D1QPpFhP.js +0 -9
- package/dist/assets/index-BdGz_GAe.css +0 -1
- package/dist/assets/index-CR9w26tq.js +0 -1
- package/dist/assets/index-vJkimYqD.js +0 -1
- package/dist/assets/main-BV_IuaBg.css +0 -1
- package/dist/assets/main-klWBzHh0.js +0 -214
|
@@ -1 +1 @@
|
|
|
1
|
-
import{ck as a}from"./main-
|
|
1
|
+
import{ck as a}from"./main-B7wf_xU_.js";import{ImpactStyle as i,NotificationType as r}from"./index-B-ttQFx4.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-B7wf_xU_.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-B7wf_xU_.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-B7wf_xU_.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-DLzFxLC1.css">
|
|
29
29
|
</head>
|
|
30
30
|
<body>
|
|
31
31
|
<div id="app"></div>
|
|
@@ -15,7 +15,9 @@
|
|
|
15
15
|
"systemPrompt": "System Prompt",
|
|
16
16
|
"whatsapp": "WhatsApp Integration",
|
|
17
17
|
"whatsappNotifications": "WhatsApp Notifications",
|
|
18
|
+
"whatsappHistory": "WhatsApp History",
|
|
18
19
|
"data": "Data",
|
|
20
|
+
"integrations": "Integrations",
|
|
19
21
|
"experimental": "Experimental",
|
|
20
22
|
"about": "About",
|
|
21
23
|
"sky": "Sky Color"
|
|
@@ -310,6 +312,12 @@
|
|
|
310
312
|
"placeholder": "Enter your global system prompt here...\n\nExamples:\n- Coding style guidelines\n- Team communication rules\n- Project-wide conventions\n- Security best practices",
|
|
311
313
|
"confirmClear": "Are you sure you want to clear the system prompt?"
|
|
312
314
|
},
|
|
315
|
+
"integrations": {
|
|
316
|
+
"whatsapp": {
|
|
317
|
+
"title": "WhatsApp",
|
|
318
|
+
"description": "Local WhatsApp (Baileys) integration."
|
|
319
|
+
}
|
|
320
|
+
},
|
|
313
321
|
"whatsapp": {
|
|
314
322
|
"title": "WhatsApp Integration",
|
|
315
323
|
"description": "Configure the local WhatsApp (Baileys) integration. The backend talks to a local server (default port 3007) using an API key.",
|
|
@@ -392,6 +400,42 @@
|
|
|
392
400
|
"testSendFailed": "Failed to send the test message."
|
|
393
401
|
}
|
|
394
402
|
},
|
|
403
|
+
"whatsappHistory": {
|
|
404
|
+
"title": "WhatsApp History",
|
|
405
|
+
"description": "Browse persisted WhatsApp conversations and inspect their message history.",
|
|
406
|
+
"openButton": "Open History",
|
|
407
|
+
"chats": "Chats",
|
|
408
|
+
"session": "Session",
|
|
409
|
+
"noSessions": "No sessions",
|
|
410
|
+
"refresh": "Refresh",
|
|
411
|
+
"loadingChats": "Loading chats...",
|
|
412
|
+
"loadingMessages": "Loading messages...",
|
|
413
|
+
"loadingOlder": "Loading older messages...",
|
|
414
|
+
"loadOlder": "Load older messages",
|
|
415
|
+
"emptyChats": "No conversations yet.",
|
|
416
|
+
"emptyMessages": "No messages with these filters.",
|
|
417
|
+
"selectChat": "Select a chat to view its history.",
|
|
418
|
+
"direction": "Direction",
|
|
419
|
+
"type": "Type",
|
|
420
|
+
"directionValues": {
|
|
421
|
+
"all": "All",
|
|
422
|
+
"inbound": "Inbound",
|
|
423
|
+
"outbound": "Outbound"
|
|
424
|
+
},
|
|
425
|
+
"typeValues": {
|
|
426
|
+
"all": "All",
|
|
427
|
+
"text": "Text",
|
|
428
|
+
"image": "Image",
|
|
429
|
+
"audio": "Audio",
|
|
430
|
+
"video": "Video",
|
|
431
|
+
"document": "Document",
|
|
432
|
+
"sticker": "Sticker",
|
|
433
|
+
"location": "Location",
|
|
434
|
+
"contact": "Contact",
|
|
435
|
+
"reaction": "Reaction",
|
|
436
|
+
"unknown": "Unknown"
|
|
437
|
+
}
|
|
438
|
+
},
|
|
395
439
|
"whatsappNotifications": {
|
|
396
440
|
"title": "WhatsApp Notifications",
|
|
397
441
|
"description": "Choose which agent events get forwarded to WhatsApp. The recipient JID receives a message whenever an enabled event fires. Requires the WhatsApp integration to be enabled and a default session paired.",
|
|
@@ -497,7 +541,22 @@
|
|
|
497
541
|
"principle4": "Transparent agent communication",
|
|
498
542
|
"specialThanks": "Special Thanks",
|
|
499
543
|
"kenneyCredit": "for the agent character models",
|
|
500
|
-
"claudeCodeCredit": "by Anthropic, the AI backbone"
|
|
544
|
+
"claudeCodeCredit": "by Anthropic, the AI backbone",
|
|
545
|
+
"autoUpdateTitle": "Auto-update",
|
|
546
|
+
"autoUpdateButton": "Update now",
|
|
547
|
+
"autoUpdateRunning": "Installing update...",
|
|
548
|
+
"autoUpdateConfirmTitle": "Update Tide Commander?",
|
|
549
|
+
"autoUpdateConfirmBody": "This will run npm install -g tide-commander@latest and then close the server. You will need to relaunch tide-commander from your terminal.",
|
|
550
|
+
"autoUpdateConfirm": "Yes, update",
|
|
551
|
+
"autoUpdateCancel": "Cancel",
|
|
552
|
+
"autoUpdateClose": "Close",
|
|
553
|
+
"autoUpdateOutput": "Install log",
|
|
554
|
+
"autoUpdateSuccess": "Update installed.",
|
|
555
|
+
"autoUpdateRestartHint": "Please restart Tide Commander from your terminal (run: tide-commander).",
|
|
556
|
+
"autoUpdateFailed": "Update failed.",
|
|
557
|
+
"autoUpdateDevMode": "Auto-update is unavailable in dev mode.",
|
|
558
|
+
"autoUpdateManualCommand": "Run this in your terminal to update manually:",
|
|
559
|
+
"autoUpdatePackageManagerNotice": "Auto-update only supports npm-installed globals. Detected: {{pm}}."
|
|
501
560
|
},
|
|
502
561
|
"shortcuts": {
|
|
503
562
|
"pressKeys": "Press keys...",
|
|
@@ -36,6 +36,16 @@
|
|
|
36
36
|
"searchResultsTitle": "Search Results:",
|
|
37
37
|
"searchResultsCount": "{{count}} results"
|
|
38
38
|
},
|
|
39
|
+
"queue": {
|
|
40
|
+
"queued": "queued",
|
|
41
|
+
"show": "Show",
|
|
42
|
+
"enforce": "Send now",
|
|
43
|
+
"delete": "Delete",
|
|
44
|
+
"tooltip": "{{count}} message(s) queued — will send when agent is idle",
|
|
45
|
+
"modal": {
|
|
46
|
+
"title": "Queued message"
|
|
47
|
+
}
|
|
48
|
+
},
|
|
39
49
|
"input": {
|
|
40
50
|
"placeholder": "Message {{agent}}...",
|
|
41
51
|
"placeholderDefault": "Type a message...",
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import * as path from 'path';
|
|
6
6
|
import * as fs from 'fs';
|
|
7
7
|
import * as os from 'os';
|
|
8
|
+
import { fileURLToPath } from 'node:url';
|
|
8
9
|
import { createLogger, sanitizeUnicode } from '../utils/index.js';
|
|
9
10
|
import { TIDE_COMMANDER_APPENDED_PROMPT } from '../prompts/tide-commander.js';
|
|
10
11
|
import { getSystemPrompt, isEchoPromptEnabled } from '../services/system-prompt-service.js';
|
|
@@ -29,6 +30,42 @@ function writePromptToFile(prompt, agentId) {
|
|
|
29
30
|
log.log(` Wrote prompt (${prompt.length} chars) to ${promptPath}`);
|
|
30
31
|
return promptPath;
|
|
31
32
|
}
|
|
33
|
+
// Headless permission-prompt MCP server. Resolves AskUserQuestion and
|
|
34
|
+
// ExitPlanMode (which would otherwise dead-lock waiting for a TUI dialog in a
|
|
35
|
+
// non-interactive Claude CLI subprocess) by auto-answering / auto-approving
|
|
36
|
+
// via the documented `--permission-prompt-tool` MCP hook.
|
|
37
|
+
const PERMISSION_PROMPT_SERVER_BASENAME = 'permission-prompt-server.mjs';
|
|
38
|
+
// CLI tool reference shape: `mcp__<server-name-in-mcp-config>__<tool-name>`.
|
|
39
|
+
const PERMISSION_PROMPT_TOOL = 'mcp__tideperm__permission_prompt';
|
|
40
|
+
/**
|
|
41
|
+
* Write the per-process mcp-config and return its path. The config registers
|
|
42
|
+
* one stdio MCP server (`tideperm`) that runs the bundled permission-prompt
|
|
43
|
+
* server script — same shape the SDK uses for permission_prompt_tool_name,
|
|
44
|
+
* but invoked through the CLI so the agent still bills against Claude Code,
|
|
45
|
+
* not the Anthropic API.
|
|
46
|
+
*/
|
|
47
|
+
function getPermissionPromptMcpConfigPath() {
|
|
48
|
+
const tideDataDir = path.join(os.homedir(), '.tide-commander');
|
|
49
|
+
if (!fs.existsSync(tideDataDir)) {
|
|
50
|
+
fs.mkdirSync(tideDataDir, { recursive: true });
|
|
51
|
+
}
|
|
52
|
+
// Resolve the bundled server script. In dev (tsx) it lives next to this
|
|
53
|
+
// file; in the prebuilt bundle the .mjs is copied alongside the .js by
|
|
54
|
+
// `npm run build:server` (see package.json).
|
|
55
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
56
|
+
const serverScriptPath = path.join(here, PERMISSION_PROMPT_SERVER_BASENAME);
|
|
57
|
+
const config = {
|
|
58
|
+
mcpServers: {
|
|
59
|
+
tideperm: {
|
|
60
|
+
command: 'node',
|
|
61
|
+
args: [serverScriptPath],
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
const configPath = path.join(tideDataDir, 'permission-prompt-mcp.json');
|
|
66
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
67
|
+
return configPath;
|
|
68
|
+
}
|
|
32
69
|
export function buildAppendedProjectInstructions(config) {
|
|
33
70
|
const sections = [
|
|
34
71
|
'## CLAUDE.md / Project instructions — Tide Commander-specific rules',
|
|
@@ -87,6 +124,11 @@ export class ClaudeBackend {
|
|
|
87
124
|
// Permission mode - bypass for autonomous agents, interactive uses hooks
|
|
88
125
|
if (config.permissionMode === 'bypass') {
|
|
89
126
|
args.push('--dangerously-skip-permissions');
|
|
127
|
+
// Route AskUserQuestion / ExitPlanMode through our auto-answer MCP server
|
|
128
|
+
// so the agent doesn't dead-lock on TUI-only dialogs.
|
|
129
|
+
const mcpConfigPath = getPermissionPromptMcpConfigPath();
|
|
130
|
+
args.push('--mcp-config', mcpConfigPath);
|
|
131
|
+
args.push('--permission-prompt-tool', PERMISSION_PROMPT_TOOL);
|
|
90
132
|
}
|
|
91
133
|
else if (config.permissionMode === 'interactive') {
|
|
92
134
|
// For interactive mode, configure the PreToolUse hook to ask for permission
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Tide Commander permission-prompt MCP server.
|
|
3
|
+
//
|
|
4
|
+
// Wired in via `claude -p --mcp-config <cfg> --permission-prompt-tool
|
|
5
|
+
// mcp__tideperm__permission_prompt`. The CLI invokes this single tool whenever
|
|
6
|
+
// a permission gate (or interactive-only tool like AskUserQuestion /
|
|
7
|
+
// ExitPlanMode) needs a decision in non-interactive mode.
|
|
8
|
+
//
|
|
9
|
+
// Behavior:
|
|
10
|
+
// - For AskUserQuestion / ExitPlanMode: POST to Tide Commander's
|
|
11
|
+
// /api/agent-prompt, which holds the request and forwards it to the UI.
|
|
12
|
+
// We block on the HTTP response (server resolves once user answers).
|
|
13
|
+
// - For any other tool: auto-allow (the parent shell already runs the agent
|
|
14
|
+
// under --dangerously-skip-permissions, so the only calls that reach us
|
|
15
|
+
// are the interactive-only ones).
|
|
16
|
+
// - If the Tide Commander API is unreachable, fall back to a safe
|
|
17
|
+
// auto-answer so the agent never hangs forever.
|
|
18
|
+
//
|
|
19
|
+
// Wire format (extracted from the Claude Code 2.1.x binary):
|
|
20
|
+
// request : tools/call args { tool_name, input, tool_use_id, permission_suggestions? }
|
|
21
|
+
// response : MCP tool_result content = JSON-encoded
|
|
22
|
+
// { behavior: "allow", updatedInput } | { behavior: "deny", message }
|
|
23
|
+
//
|
|
24
|
+
// Env vars:
|
|
25
|
+
// TIDE_SERVER - base URL of Tide Commander (set by process-lifecycle.ts)
|
|
26
|
+
// TIDE_AGENT_ID - the spawning agent's id (set by process-lifecycle.ts)
|
|
27
|
+
// AUTH_TOKEN - shared auth token for /api requests
|
|
28
|
+
|
|
29
|
+
import { createInterface } from 'node:readline';
|
|
30
|
+
import { appendFileSync, mkdirSync } from 'node:fs';
|
|
31
|
+
import { dirname, join } from 'node:path';
|
|
32
|
+
import { homedir } from 'node:os';
|
|
33
|
+
|
|
34
|
+
const LOG_FILE = join(homedir(), '.tide-commander', 'logs', 'permission-prompt.jsonl');
|
|
35
|
+
try { mkdirSync(dirname(LOG_FILE), { recursive: true }); } catch {}
|
|
36
|
+
|
|
37
|
+
function log(obj) {
|
|
38
|
+
try { appendFileSync(LOG_FILE, JSON.stringify({ t: Date.now(), ...obj }) + '\n'); } catch {}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function send(msg) {
|
|
42
|
+
process.stdout.write(JSON.stringify(msg) + '\n');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const TOOL_NAME = 'permission_prompt';
|
|
46
|
+
const SERVER_INFO = { name: 'tideperm', version: '1.1.0' };
|
|
47
|
+
const TIDE_SERVER = process.env.TIDE_SERVER || 'http://localhost:5174';
|
|
48
|
+
const TIDE_AGENT_ID = process.env.TIDE_AGENT_ID || '';
|
|
49
|
+
const AUTH_TOKEN = process.env.AUTH_TOKEN || 'abcd';
|
|
50
|
+
|
|
51
|
+
const TOOL_SCHEMA = {
|
|
52
|
+
name: TOOL_NAME,
|
|
53
|
+
description: 'Tide Commander permission-prompt handler for headless --print runs. Routes AskUserQuestion / ExitPlanMode to the UI.',
|
|
54
|
+
inputSchema: {
|
|
55
|
+
type: 'object',
|
|
56
|
+
properties: {
|
|
57
|
+
tool_name: { type: 'string' },
|
|
58
|
+
input: { type: 'object', additionalProperties: true },
|
|
59
|
+
tool_use_id: { type: 'string' },
|
|
60
|
+
permission_suggestions: { type: 'array' },
|
|
61
|
+
},
|
|
62
|
+
required: ['tool_name'],
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
function autoAnswer(args) {
|
|
67
|
+
const toolName = args.tool_name;
|
|
68
|
+
const input = args.input || {};
|
|
69
|
+
if (toolName === 'AskUserQuestion') {
|
|
70
|
+
const questions = Array.isArray(input.questions) ? input.questions : [];
|
|
71
|
+
const answers = {};
|
|
72
|
+
for (const q of questions) {
|
|
73
|
+
const firstOpt = Array.isArray(q.options) && q.options[0]
|
|
74
|
+
? q.options[0].label
|
|
75
|
+
: 'OK';
|
|
76
|
+
answers[q && q.question] = firstOpt;
|
|
77
|
+
}
|
|
78
|
+
return { behavior: 'allow', updatedInput: { questions, answers } };
|
|
79
|
+
}
|
|
80
|
+
return { behavior: 'allow', updatedInput: input };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function askUiForDecision(args) {
|
|
84
|
+
const toolName = args.tool_name;
|
|
85
|
+
if (toolName !== 'AskUserQuestion' && toolName !== 'ExitPlanMode') {
|
|
86
|
+
return autoAnswer(args);
|
|
87
|
+
}
|
|
88
|
+
if (!TIDE_AGENT_ID) {
|
|
89
|
+
log({ event: 'no_agent_id', toolName });
|
|
90
|
+
return autoAnswer(args);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const requestId = args.tool_use_id || `tideperm_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
94
|
+
const body = {
|
|
95
|
+
id: requestId,
|
|
96
|
+
agentId: TIDE_AGENT_ID,
|
|
97
|
+
tool: toolName,
|
|
98
|
+
input: args.input || {},
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
const res = await fetch(`${TIDE_SERVER}/api/agent-prompt`, {
|
|
103
|
+
method: 'POST',
|
|
104
|
+
headers: {
|
|
105
|
+
'Content-Type': 'application/json',
|
|
106
|
+
'X-Auth-Token': AUTH_TOKEN,
|
|
107
|
+
},
|
|
108
|
+
body: JSON.stringify(body),
|
|
109
|
+
});
|
|
110
|
+
if (!res.ok) {
|
|
111
|
+
log({ event: 'http_error', toolName, status: res.status });
|
|
112
|
+
return autoAnswer(args);
|
|
113
|
+
}
|
|
114
|
+
const payload = await res.json();
|
|
115
|
+
log({ event: 'ui_responded', requestId, approved: payload.approved });
|
|
116
|
+
|
|
117
|
+
if (!payload.approved) {
|
|
118
|
+
return {
|
|
119
|
+
behavior: 'deny',
|
|
120
|
+
message: payload.reason || 'User declined',
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (toolName === 'AskUserQuestion') {
|
|
125
|
+
const questions = Array.isArray(args.input?.questions) ? args.input.questions : [];
|
|
126
|
+
return {
|
|
127
|
+
behavior: 'allow',
|
|
128
|
+
updatedInput: { questions, answers: payload.answers || {} },
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return { behavior: 'allow', updatedInput: args.input || {} };
|
|
133
|
+
} catch (err) {
|
|
134
|
+
log({ event: 'fetch_failed', toolName, error: String(err) });
|
|
135
|
+
return autoAnswer(args);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const rl = createInterface({ input: process.stdin, terminal: false });
|
|
140
|
+
rl.on('line', async (line) => {
|
|
141
|
+
if (!line.trim()) return;
|
|
142
|
+
let req;
|
|
143
|
+
try { req = JSON.parse(line); } catch (e) {
|
|
144
|
+
log({ event: 'parse_error', error: String(e), line: line.slice(0, 200) });
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
const { jsonrpc = '2.0', id, method, params } = req;
|
|
148
|
+
|
|
149
|
+
if (method === 'initialize') {
|
|
150
|
+
send({
|
|
151
|
+
jsonrpc, id,
|
|
152
|
+
result: {
|
|
153
|
+
protocolVersion: params?.protocolVersion || '2024-11-05',
|
|
154
|
+
capabilities: { tools: {} },
|
|
155
|
+
serverInfo: SERVER_INFO,
|
|
156
|
+
},
|
|
157
|
+
});
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (method === 'notifications/initialized') return;
|
|
162
|
+
|
|
163
|
+
if (method === 'tools/list') {
|
|
164
|
+
send({ jsonrpc, id, result: { tools: [TOOL_SCHEMA] } });
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (method === 'tools/call') {
|
|
169
|
+
const { name, arguments: args } = params || {};
|
|
170
|
+
if (name !== TOOL_NAME) {
|
|
171
|
+
send({ jsonrpc, id, error: { code: -32601, message: `Unknown tool: ${name}` } });
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
const decision = await askUiForDecision(args || {});
|
|
175
|
+
log({ event: 'decide', toolName: args?.tool_name, decisionBehavior: decision.behavior });
|
|
176
|
+
send({
|
|
177
|
+
jsonrpc, id,
|
|
178
|
+
result: { content: [{ type: 'text', text: JSON.stringify(decision) }] },
|
|
179
|
+
});
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (id !== undefined) {
|
|
184
|
+
send({ jsonrpc, id, error: { code: -32601, message: `Unknown method: ${method}` } });
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
log({ event: 'startup', pid: process.pid, agentId: TIDE_AGENT_ID || '<none>', server: TIDE_SERVER });
|
|
@@ -51,6 +51,9 @@ export class RunnerProcessLifecycle {
|
|
|
51
51
|
LANG: 'en_US.UTF-8',
|
|
52
52
|
LC_ALL: 'en_US.UTF-8',
|
|
53
53
|
TIDE_SERVER: `http://localhost:${process.env.TIDE_PORT || process.env.PORT || 5174}`,
|
|
54
|
+
// Used by the MCP permission-prompt server (see backend.ts) to route
|
|
55
|
+
// AskUserQuestion / ExitPlanMode back to this agent's UI thread.
|
|
56
|
+
TIDE_AGENT_ID: agentId,
|
|
54
57
|
...extraEnv,
|
|
55
58
|
};
|
|
56
59
|
// ---- tmux mode ----
|
|
@@ -190,6 +190,14 @@ export class RunnerStdoutPipeline {
|
|
|
190
190
|
}
|
|
191
191
|
if (event.permissionDenials && event.permissionDenials.length > 0) {
|
|
192
192
|
for (const denial of event.permissionDenials) {
|
|
193
|
+
// Suppress the "[System] Permission denied" line for the two tools
|
|
194
|
+
// routed through our MCP perm-prompt server (AskUserQuestion,
|
|
195
|
+
// ExitPlanMode). For those, the user rejecting via the inline UI
|
|
196
|
+
// already conveys the outcome; surfacing the CLI's generic denial
|
|
197
|
+
// is just noise that looks like a system error.
|
|
198
|
+
if (denial.toolName === 'AskUserQuestion' || denial.toolName === 'AskFollowupQuestion' || denial.toolName === 'ExitPlanMode') {
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
193
201
|
const denialSummary = this.formatPermissionDenialSummary(denial.toolName, denial.toolInput);
|
|
194
202
|
this.callbacks.onOutput(agentId, `[System] Permission denied: ${denialSummary}`, false, undefined, event.uuid);
|
|
195
203
|
}
|
|
@@ -104,6 +104,19 @@ export function spawnInTmux(executable, args, options) {
|
|
|
104
104
|
else {
|
|
105
105
|
fullCmd = `${sttyPrefix}${executable} ${escapedArgs} > '${logFile}' 2> '${stderrFile}'`;
|
|
106
106
|
}
|
|
107
|
+
// Env vars to push into the new tmux session. We use `-e KEY=VAL` on
|
|
108
|
+
// `new-session` because tmux's session env is inherited from the *tmux
|
|
109
|
+
// server*, not the new-session client — and the server may have been
|
|
110
|
+
// started long ago with a stale env (missing newer Tide Commander vars like
|
|
111
|
+
// TIDE_AGENT_ID). `-e` overrides on a per-session basis. We only push the
|
|
112
|
+
// Tide-specific keys; everything else falls back to the server env.
|
|
113
|
+
const envFlags = [];
|
|
114
|
+
for (const key of ['TIDE_SERVER', 'TIDE_AGENT_ID', 'AUTH_TOKEN']) {
|
|
115
|
+
const val = options.env[key];
|
|
116
|
+
if (val !== undefined && val !== '') {
|
|
117
|
+
envFlags.push('-e', `${key}=${val}`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
107
120
|
// Spawn the tmux session
|
|
108
121
|
const launcherProcess = spawn('tmux', [
|
|
109
122
|
'new-session',
|
|
@@ -111,6 +124,7 @@ export function spawnInTmux(executable, args, options) {
|
|
|
111
124
|
'-s', sessionName, // session name
|
|
112
125
|
'-x', '200', // width
|
|
113
126
|
'-y', '50', // height
|
|
127
|
+
...envFlags, // per-session env (overrides stale tmux-server env)
|
|
114
128
|
'--', 'sh', '-c', fullCmd,
|
|
115
129
|
], {
|
|
116
130
|
cwd: options.cwd,
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Pre-built query functions organized by domain.
|
|
4
4
|
* Each integration calls these instead of writing raw SQL.
|
|
5
5
|
*/
|
|
6
|
-
import { insertOne, queryMany, queryOne, execute } from './event-db.js';
|
|
6
|
+
import { insertOne, queryMany, queryOne, execute, getDb } from './event-db.js';
|
|
7
7
|
// ─── JSON serialization helpers ───
|
|
8
8
|
function toJson(value) {
|
|
9
9
|
if (value === undefined || value === null)
|
|
@@ -95,6 +95,110 @@ export function logSlackMessage(msg) {
|
|
|
95
95
|
integration_instance_id: msg.integrationInstanceId ?? 'default',
|
|
96
96
|
});
|
|
97
97
|
}
|
|
98
|
+
function whatsappRowToEvent(row) {
|
|
99
|
+
return {
|
|
100
|
+
id: row.id,
|
|
101
|
+
sessionId: row.session_id,
|
|
102
|
+
messageId: row.message_id ?? undefined,
|
|
103
|
+
chatId: row.chat_id,
|
|
104
|
+
isGroup: row.is_group === 1,
|
|
105
|
+
groupName: row.group_name ?? undefined,
|
|
106
|
+
fromJid: row.from_jid,
|
|
107
|
+
fromName: row.from_name ?? undefined,
|
|
108
|
+
direction: row.direction,
|
|
109
|
+
body: row.body,
|
|
110
|
+
messageType: row.message_type,
|
|
111
|
+
mediaMimetype: row.media_mimetype ?? undefined,
|
|
112
|
+
mediaSize: row.media_size ?? undefined,
|
|
113
|
+
mediaFilename: row.media_filename ?? undefined,
|
|
114
|
+
mediaPath: row.media_path ?? undefined,
|
|
115
|
+
audioTranscription: row.audio_transcription ?? undefined,
|
|
116
|
+
agentId: row.agent_id ?? undefined,
|
|
117
|
+
workflowInstanceId: row.workflow_instance_id ?? undefined,
|
|
118
|
+
rawEvent: fromJson(row.raw_event),
|
|
119
|
+
timestamp: row.timestamp,
|
|
120
|
+
receivedAt: row.received_at,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
const WHATSAPP_INSERT_SQL = `
|
|
124
|
+
INSERT OR IGNORE INTO whatsapp_messages (
|
|
125
|
+
session_id, message_id, chat_id, is_group, group_name,
|
|
126
|
+
from_jid, from_name, direction, body, message_type,
|
|
127
|
+
media_mimetype, media_size, media_filename, media_path, audio_transcription,
|
|
128
|
+
agent_id, workflow_instance_id, raw_event, timestamp, received_at
|
|
129
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
130
|
+
`;
|
|
131
|
+
export function logWhatsAppMessage(msg) {
|
|
132
|
+
const stmt = getDb().prepare(WHATSAPP_INSERT_SQL);
|
|
133
|
+
const result = stmt.run(msg.sessionId, msg.messageId ?? null, msg.chatId, msg.isGroup ? 1 : 0, msg.groupName ?? null, msg.fromJid, msg.fromName ?? null, msg.direction, msg.body, msg.messageType, msg.mediaMimetype ?? null, msg.mediaSize ?? null, msg.mediaFilename ?? null, msg.mediaPath ?? null, msg.audioTranscription ?? null, msg.agentId ?? null, msg.workflowInstanceId ?? null, toJson(msg.rawEvent), msg.timestamp, msg.receivedAt);
|
|
134
|
+
return Number(result.lastInsertRowid);
|
|
135
|
+
}
|
|
136
|
+
export function getWhatsAppMessagesByChat(sessionId, chatId, limit = 50) {
|
|
137
|
+
const rows = queryMany(`SELECT * FROM whatsapp_messages
|
|
138
|
+
WHERE session_id = ? AND chat_id = ?
|
|
139
|
+
ORDER BY timestamp DESC
|
|
140
|
+
LIMIT ?`, [sessionId, chatId, limit]);
|
|
141
|
+
return rows.map(whatsappRowToEvent);
|
|
142
|
+
}
|
|
143
|
+
export function getWhatsAppMessagesByChatPaged(sessionId, chatId, opts = {}) {
|
|
144
|
+
const limit = Math.min(Math.max(1, opts.limit ?? 50), 200);
|
|
145
|
+
const clauses = ['session_id = ?', 'chat_id = ?'];
|
|
146
|
+
const params = [sessionId, chatId];
|
|
147
|
+
if (typeof opts.cursor === 'number' && Number.isFinite(opts.cursor)) {
|
|
148
|
+
clauses.push('timestamp < ?');
|
|
149
|
+
params.push(opts.cursor);
|
|
150
|
+
}
|
|
151
|
+
if (opts.direction) {
|
|
152
|
+
clauses.push('direction = ?');
|
|
153
|
+
params.push(opts.direction);
|
|
154
|
+
}
|
|
155
|
+
if (opts.type) {
|
|
156
|
+
clauses.push('message_type = ?');
|
|
157
|
+
params.push(opts.type);
|
|
158
|
+
}
|
|
159
|
+
params.push(limit + 1);
|
|
160
|
+
const rows = queryMany(`SELECT * FROM whatsapp_messages
|
|
161
|
+
WHERE ${clauses.join(' AND ')}
|
|
162
|
+
ORDER BY timestamp DESC
|
|
163
|
+
LIMIT ?`, params);
|
|
164
|
+
const hasMore = rows.length > limit;
|
|
165
|
+
const page = hasMore ? rows.slice(0, limit) : rows;
|
|
166
|
+
const nextCursor = hasMore ? page[page.length - 1].timestamp : null;
|
|
167
|
+
return { messages: page.map(whatsappRowToEvent), nextCursor };
|
|
168
|
+
}
|
|
169
|
+
export function getWhatsAppChatsList(sessionId) {
|
|
170
|
+
const rows = queryMany(`SELECT
|
|
171
|
+
wm.chat_id AS chat_id,
|
|
172
|
+
wm.timestamp AS last_ts,
|
|
173
|
+
agg.msg_count AS msg_count,
|
|
174
|
+
wm.body AS body,
|
|
175
|
+
wm.message_type AS message_type,
|
|
176
|
+
wm.direction AS direction,
|
|
177
|
+
wm.is_group AS is_group,
|
|
178
|
+
wm.group_name AS group_name,
|
|
179
|
+
wm.from_name AS from_name
|
|
180
|
+
FROM whatsapp_messages wm
|
|
181
|
+
INNER JOIN (
|
|
182
|
+
SELECT chat_id, MAX(timestamp) AS last_ts, COUNT(*) AS msg_count
|
|
183
|
+
FROM whatsapp_messages
|
|
184
|
+
WHERE session_id = ?
|
|
185
|
+
GROUP BY chat_id
|
|
186
|
+
) agg ON agg.chat_id = wm.chat_id AND agg.last_ts = wm.timestamp
|
|
187
|
+
WHERE wm.session_id = ?
|
|
188
|
+
ORDER BY wm.timestamp DESC`, [sessionId, sessionId]);
|
|
189
|
+
return rows.map((row) => ({
|
|
190
|
+
chatId: row.chat_id,
|
|
191
|
+
lastTimestamp: row.last_ts,
|
|
192
|
+
lastMessagePreview: row.body.length > 100 ? `${row.body.slice(0, 100)}…` : row.body,
|
|
193
|
+
lastMessageType: row.message_type,
|
|
194
|
+
lastDirection: row.direction,
|
|
195
|
+
messageCount: row.msg_count,
|
|
196
|
+
isGroup: row.is_group === 1,
|
|
197
|
+
groupName: row.group_name ?? undefined,
|
|
198
|
+
fromName: row.from_name ?? undefined,
|
|
199
|
+
unreadCount: 0,
|
|
200
|
+
}));
|
|
201
|
+
}
|
|
98
202
|
function emailRowToEvent(row) {
|
|
99
203
|
return {
|
|
100
204
|
id: row.id,
|
|
@@ -674,6 +778,44 @@ export function querySlackMessages(opts) {
|
|
|
674
778
|
const rows = queryMany(`SELECT * FROM slack_messages${where} ORDER BY received_at DESC LIMIT ?`, dataParams);
|
|
675
779
|
return { messages: rows.map(slackRowToEvent), total };
|
|
676
780
|
}
|
|
781
|
+
export function queryWhatsAppMessages(opts) {
|
|
782
|
+
const conditions = [];
|
|
783
|
+
const params = [];
|
|
784
|
+
if (opts.sessionId) {
|
|
785
|
+
conditions.push('session_id = ?');
|
|
786
|
+
params.push(opts.sessionId);
|
|
787
|
+
}
|
|
788
|
+
if (opts.chatId) {
|
|
789
|
+
conditions.push('chat_id = ?');
|
|
790
|
+
params.push(opts.chatId);
|
|
791
|
+
}
|
|
792
|
+
if (opts.direction) {
|
|
793
|
+
conditions.push('direction = ?');
|
|
794
|
+
params.push(opts.direction);
|
|
795
|
+
}
|
|
796
|
+
if (opts.messageType) {
|
|
797
|
+
conditions.push('message_type = ?');
|
|
798
|
+
params.push(opts.messageType);
|
|
799
|
+
}
|
|
800
|
+
if (opts.workflowInstanceId) {
|
|
801
|
+
conditions.push('workflow_instance_id = ?');
|
|
802
|
+
params.push(opts.workflowInstanceId);
|
|
803
|
+
}
|
|
804
|
+
if (opts.agentId) {
|
|
805
|
+
conditions.push('agent_id = ?');
|
|
806
|
+
params.push(opts.agentId);
|
|
807
|
+
}
|
|
808
|
+
if (opts.since) {
|
|
809
|
+
conditions.push('timestamp >= ?');
|
|
810
|
+
params.push(opts.since);
|
|
811
|
+
}
|
|
812
|
+
const where = conditions.length > 0 ? ' WHERE ' + conditions.join(' AND ') : '';
|
|
813
|
+
const countRow = queryOne(`SELECT COUNT(*) as count FROM whatsapp_messages${where}`, params);
|
|
814
|
+
const total = countRow?.count ?? 0;
|
|
815
|
+
const dataParams = [...params, opts.limit ?? 50];
|
|
816
|
+
const rows = queryMany(`SELECT * FROM whatsapp_messages${where} ORDER BY timestamp DESC LIMIT ?`, dataParams);
|
|
817
|
+
return { messages: rows.map(whatsappRowToEvent), total };
|
|
818
|
+
}
|
|
677
819
|
export function queryEmailMessages(opts) {
|
|
678
820
|
const conditions = [];
|
|
679
821
|
const params = [];
|