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.
Files changed (71) hide show
  1. package/dist/assets/{BossLogsModal-CT25hD17.js → BossLogsModal-CDel834o.js} +1 -1
  2. package/dist/assets/{BossSpawnModal-9rS7AFkZ.js → BossSpawnModal-BB9wL5VV.js} +1 -1
  3. package/dist/assets/{ControlsModal-D-mymoM7.js → ControlsModal-D5RE5MvT.js} +1 -1
  4. package/dist/assets/{DockerLogsModal-Ae-ZCeeP.js → DockerLogsModal-B27P1JpZ.js} +1 -1
  5. package/dist/assets/{EmbeddedEditor-DLOOpM0K.js → EmbeddedEditor-DP1jqsT_.js} +1 -1
  6. package/dist/assets/{GmailOAuthSetup-C9NLhWLo.js → GmailOAuthSetup-DvuL5G8Q.js} +1 -1
  7. package/dist/assets/{GoogleOAuthSetup-1kzgrPV6.js → GoogleOAuthSetup-CG6bSCjv.js} +1 -1
  8. package/dist/assets/{IframeModal-DKS0IFsr.js → IframeModal-ClnUGmJV.js} +1 -1
  9. package/dist/assets/{IntegrationsPanel-CBvKOeud.js → IntegrationsPanel-0vdfxZRq.js} +2 -2
  10. package/dist/assets/{LogViewerModal-Dlt8JfVg.js → LogViewerModal-DLQlrZ4O.js} +1 -1
  11. package/dist/assets/{MonitoringModal-BM1IEZv6.js → MonitoringModal-DiC9TNCy.js} +1 -1
  12. package/dist/assets/{PM2LogsModal-B1-HUHWZ.js → PM2LogsModal-BgPrnaP5.js} +1 -1
  13. package/dist/assets/{RestoreArchivedAreaModal-DXmYo7fp.js → RestoreArchivedAreaModal-CIN1OrOW.js} +1 -1
  14. package/dist/assets/{Scene2DCanvas-CuUxSaPb.js → Scene2DCanvas-Bap6brvv.js} +1 -1
  15. package/dist/assets/{SceneManager-UD3IHY20.js → SceneManager-CidCW0PR.js} +1 -1
  16. package/dist/assets/{SkillsPanel-DjRBVrO2.js → SkillsPanel-DHQTPaP2.js} +1 -1
  17. package/dist/assets/{SlackMultiInstanceSetup-Csp81Dqn.js → SlackMultiInstanceSetup-CQK4D89W.js} +1 -1
  18. package/dist/assets/{SpawnModal-dg0mH3d9.js → SpawnModal-Cx_k3HHC.js} +1 -1
  19. package/dist/assets/{SubordinateAssignmentModal-CeBPRNNX.js → SubordinateAssignmentModal-C1DOv51H.js} +1 -1
  20. package/dist/assets/TriggerManagerPanel-jP5RBK2L.js +9 -0
  21. package/dist/assets/{WorkflowEditorPanel-IIsptZgp.js → WorkflowEditorPanel-Dh6mZ8M4.js} +1 -1
  22. package/dist/assets/{index-h-IcmGfB.js → index-B-ttQFx4.js} +2 -2
  23. package/dist/assets/index-BXnThzaG.js +11 -0
  24. package/dist/assets/index-CK8NcQSU.css +1 -0
  25. package/dist/assets/{index-CNDUxsGy.js → index-CYwFXTQZ.js} +1 -1
  26. package/dist/assets/{index-sDgBtEgH.js → index-Cwlm-Pqi.js} +3 -3
  27. package/dist/assets/index-D96LXKm4.js +1 -0
  28. package/dist/assets/{index-BGh9tRSy.js → index-DP5sMNS9.js} +1 -1
  29. package/dist/assets/{index-CsyPNc8u.js → index-DfEbuBH8.js} +1 -1
  30. package/dist/assets/{index-DEI-vrXk.js → index-DsKaX6TJ.js} +1 -1
  31. package/dist/assets/{index-CIqkVLo1.js → index-enJvXAbe.js} +1 -1
  32. package/dist/assets/main-B7wf_xU_.js +214 -0
  33. package/dist/assets/main-DLzFxLC1.css +1 -0
  34. package/dist/assets/{web-BmPSJLwQ.js → web-BHmmnvF7.js} +1 -1
  35. package/dist/assets/{web-Dggt4D4N.js → web-IGuhG0xr.js} +1 -1
  36. package/dist/assets/{web-BgPjNMBK.js → web-SOehUGgT.js} +1 -1
  37. package/dist/index.html +2 -2
  38. package/dist/locales/en/config.json +60 -1
  39. package/dist/locales/en/terminal.json +10 -0
  40. package/dist/src/packages/server/claude/backend.js +42 -0
  41. package/dist/src/packages/server/claude/permission-prompt-server.mjs +188 -0
  42. package/dist/src/packages/server/claude/runner/process-lifecycle.js +3 -0
  43. package/dist/src/packages/server/claude/runner/stdout-pipeline.js +8 -0
  44. package/dist/src/packages/server/claude/runner/tmux-helper.js +14 -0
  45. package/dist/src/packages/server/data/event-queries.js +143 -1
  46. package/dist/src/packages/server/data/migrations/007_whatsapp_messages.sql +48 -0
  47. package/dist/src/packages/server/index.js +1 -0
  48. package/dist/src/packages/server/integrations/gmail/gmail-client.js +139 -24
  49. package/dist/src/packages/server/integrations/gmail/gmail-routes.js +162 -0
  50. package/dist/src/packages/server/integrations/whatsapp/whatsapp-routes.js +81 -0
  51. package/dist/src/packages/server/integrations/whatsapp/whatsapp-trigger-handler.js +29 -0
  52. package/dist/src/packages/server/routes/agent-prompt.js +57 -0
  53. package/dist/src/packages/server/routes/index.js +8 -1
  54. package/dist/src/packages/server/routes/skills.js +193 -0
  55. package/dist/src/packages/server/routes/system.js +156 -0
  56. package/dist/src/packages/server/routes/trigger-routes.js +74 -17
  57. package/dist/src/packages/server/routes/webhook-signatures.js +20 -7
  58. package/dist/src/packages/server/services/agent-prompt-service.js +100 -0
  59. package/dist/src/packages/server/services/index.js +1 -0
  60. package/dist/src/packages/server/services/self-update-service.js +191 -0
  61. package/dist/src/packages/server/websocket/handler.js +2 -1
  62. package/dist/src/packages/server/websocket/listeners/agent-prompt-listeners.js +13 -0
  63. package/dist/src/packages/server/websocket/listeners/index.js +2 -0
  64. package/dist/src/packages/shared/whatsapp-types.js +1 -0
  65. package/package.json +2 -2
  66. package/dist/assets/TriggerManagerPanel-D1QPpFhP.js +0 -9
  67. package/dist/assets/index-BdGz_GAe.css +0 -1
  68. package/dist/assets/index-CR9w26tq.js +0 -1
  69. package/dist/assets/index-vJkimYqD.js +0 -1
  70. package/dist/assets/main-BV_IuaBg.css +0 -1
  71. package/dist/assets/main-klWBzHh0.js +0 -214
@@ -1 +1 @@
1
- import{ck as a}from"./main-klWBzHh0.js";import{ImpactStyle as i,NotificationType as r}from"./index-h-IcmGfB.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
+ 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-klWBzHh0.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
+ 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-klWBzHh0.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};
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-klWBzHh0.js"></script>
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-BV_IuaBg.css">
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 = [];