tide-commander 1.160.0 → 1.161.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 (50) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/assets/{BossLogsModal-COg0RBw_.js → BossLogsModal-ByN3qs9-.js} +1 -1
  3. package/dist/assets/{BossSpawnModal-Bvkk2uFi.js → BossSpawnModal-CNlqXvK9.js} +1 -1
  4. package/dist/assets/{ControlsModal-BWcWGs2x.js → ControlsModal-BxKH1QLw.js} +1 -1
  5. package/dist/assets/{DockerLogsModal-C4tJ59Pf.js → DockerLogsModal-BPGru2EX.js} +1 -1
  6. package/dist/assets/{EmbeddedEditor-DiDm5EvF.js → EmbeddedEditor-kSj9yTQI.js} +1 -1
  7. package/dist/assets/{GmailOAuthSetup-GkYs6wUs.js → GmailOAuthSetup-aaL2rsLN.js} +1 -1
  8. package/dist/assets/{GoogleOAuthSetup-KKwp2gEQ.js → GoogleOAuthSetup-Bc61FQ2H.js} +1 -1
  9. package/dist/assets/{IframeModal-D0iTkxZB.js → IframeModal-DvM559FE.js} +1 -1
  10. package/dist/assets/{IntegrationsPanel-CCKoizJa.js → IntegrationsPanel-4_733lDP.js} +2 -2
  11. package/dist/assets/{LogViewerModal-CSGEkyhZ.js → LogViewerModal-BjC13hIK.js} +1 -1
  12. package/dist/assets/{MonitoringModal-Be6uEd7K.js → MonitoringModal-Xjkid3A5.js} +1 -1
  13. package/dist/assets/{PM2LogsModal-DZG7Tcw7.js → PM2LogsModal-DpbDESnu.js} +1 -1
  14. package/dist/assets/{RestoreArchivedAreaModal-CigBbBB_.js → RestoreArchivedAreaModal-B-9zHb3p.js} +1 -1
  15. package/dist/assets/{Scene2DCanvas-sJig3vJ4.js → Scene2DCanvas-DQdSAptp.js} +1 -1
  16. package/dist/assets/{SceneManager-BG_0UVJc.js → SceneManager-Bn-9k6mM.js} +1 -1
  17. package/dist/assets/{SkillsPanel-BnAf8uyn.js → SkillsPanel-Bk3rbNQB.js} +1 -1
  18. package/dist/assets/{SlackMultiInstanceSetup-DGuexHRz.js → SlackMultiInstanceSetup-CfhQ10Um.js} +1 -1
  19. package/dist/assets/{SpawnModal-FjDJZlIm.js → SpawnModal-DfT7bzNo.js} +1 -1
  20. package/dist/assets/{StatisticsModal-CbbdwSF0.js → StatisticsModal-DFDFE58F.js} +1 -1
  21. package/dist/assets/{SubordinateAssignmentModal-Dn2uuOcT.js → SubordinateAssignmentModal-hqH3o5Lp.js} +1 -1
  22. package/dist/assets/{TriggerManagerPanel-Cj9pqf-r.js → TriggerManagerPanel-svSECUjd.js} +1 -1
  23. package/dist/assets/{WorkflowEditorPanel-nucrx_0v.js → WorkflowEditorPanel-CIqg8BLi.js} +1 -1
  24. package/dist/assets/{area-logos-C93KZk3h.js → area-logos-B0efSjrz.js} +1 -1
  25. package/dist/assets/{index-CmmI2kZf.js → index-B8R_gEnb.js} +2 -2
  26. package/dist/assets/{index-Gjc7NXlZ.js → index-BeQoCc1W.js} +1 -1
  27. package/dist/assets/{index-ojmo-Cva.js → index-CI4TPz4h.js} +1 -1
  28. package/dist/assets/index-CMI4WoS7.js +2 -0
  29. package/dist/assets/index-CPVGsE7g.js +19 -0
  30. package/dist/assets/{index-BkbKu66S.js → index-CmiQbHNF.js} +1 -1
  31. package/dist/assets/{index-D7YoMagO.js → index-Db0U-21f.js} +1 -1
  32. package/dist/assets/{index-fh53j_4E.js → index-F4gvAXEg.js} +1 -1
  33. package/dist/assets/{index-iX9jPnEp.js → index-hzbuivYC.js} +1 -1
  34. package/dist/assets/{main-BZcOdtO_.css → main-BX-OUL7k.css} +1 -1
  35. package/dist/assets/{main-BFW0rGXU.js → main-Bwg5cpUh.js} +121 -121
  36. package/dist/assets/{web-M9cLwTrs.js → web-AyXD75n2.js} +1 -1
  37. package/dist/assets/{web-8gl2noUE.js → web-D9FjjUJx.js} +1 -1
  38. package/dist/assets/{web-D0uiHhnA.js → web-zhILDfYB.js} +1 -1
  39. package/dist/build-info.json +1 -1
  40. package/dist/index.html +2 -2
  41. package/dist/locales/en/terminal.json +2 -0
  42. package/dist/locales/es/terminal.json +1 -0
  43. package/dist/src/packages/server/claude/session-loader.js +98 -0
  44. package/dist/src/packages/server/codex/json-event-parser.js +99 -0
  45. package/dist/src/packages/server/index.js +4 -0
  46. package/dist/src/packages/server/services/claude-credentials-service.js +172 -13
  47. package/dist/src/packages/server/services/claude-usage-service.js +5 -1
  48. package/package.json +1 -1
  49. package/dist/assets/index-DIOhr3im.js +0 -19
  50. package/dist/assets/index-DqM0QltS.js +0 -2
@@ -1 +1 @@
1
- import{df as t}from"./main-BFW0rGXU.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{dh as t}from"./main-Bwg5cpUh.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 +1 @@
1
- import{df as a}from"./main-BFW0rGXU.js";import{ImpactStyle as i,NotificationType as r}from"./index-CmmI2kZf.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{dh as a}from"./main-Bwg5cpUh.js";import{ImpactStyle as i,NotificationType as r}from"./index-B8R_gEnb.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{df as s}from"./main-BFW0rGXU.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{dh as s}from"./main-Bwg5cpUh.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
- {"version":"1.160.0"}
1
+ {"version":"1.161.0"}
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-BFW0rGXU.js"></script>
25
+ <script type="module" crossorigin src="/assets/main-Bwg5cpUh.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-BZcOdtO_.css">
28
+ <link rel="stylesheet" crossorigin href="/assets/main-BX-OUL7k.css">
29
29
  </head>
30
30
  <body>
31
31
  <div id="app"></div>
@@ -1285,6 +1285,7 @@
1285
1285
  "currentSession": "Current session",
1286
1286
  "currentWeekAll": "Current week (all models)",
1287
1287
  "currentWeekOpus": "Current week (Opus only)",
1288
+ "currentWeekFable": "Current week (Fable)",
1288
1289
  "currentWeekSonnet": "Current week (Sonnet only)",
1289
1290
  "weeklyLimit": "Weekly limit",
1290
1291
  "monthlyLimit": "Monthly limit",
@@ -1327,6 +1328,7 @@
1327
1328
  "gaugeSession": "Session",
1328
1329
  "gaugeWeek": "Week",
1329
1330
  "gaugeWeekOpus": "Opus wk",
1331
+ "gaugeWeekFable": "Fable wk",
1330
1332
  "gaugeNoSession": "no active session",
1331
1333
  "providerTitle": "{{provider}} Accounts",
1332
1334
  "providerHelp": "Named copies of {{dir}}/auth.json. Switch the active {{provider}} account when you hit rate limits — same as renaming the file manually."
@@ -1137,6 +1137,7 @@
1137
1137
  "gaugeSession": "Sesión",
1138
1138
  "gaugeWeek": "Semana",
1139
1139
  "gaugeWeekOpus": "Opus sem",
1140
+ "gaugeWeekFable": "Fable sem",
1140
1141
  "gaugeNoSession": "sin sesión activa",
1141
1142
  "providerTitle": "Cuentas de {{provider}}",
1142
1143
  "providerHelp": "Copias nombradas de {{dir}}/auth.json. Cambia la cuenta activa de {{provider}} cuando llegues a los límites — igual que renombrar el archivo a mano."
@@ -348,6 +348,9 @@ function normalizeCodexWebSearchToolInput(payload) {
348
348
  ? actionQueriesRaw
349
349
  : undefined;
350
350
  return {
351
+ query: typeof payload.query === 'string'
352
+ ? payload.query
353
+ : typeof action.query === 'string' ? action.query : undefined,
351
354
  actionType: typeof action.type === 'string' ? action.type : undefined,
352
355
  actionQuery: typeof action.query === 'string' ? action.query : undefined,
353
356
  actionQueries,
@@ -355,6 +358,40 @@ function normalizeCodexWebSearchToolInput(payload) {
355
358
  status: typeof payload.status === 'string' ? payload.status : undefined,
356
359
  };
357
360
  }
361
+ function normalizeCodexMcpToolCall(payload) {
362
+ if (!isObject(payload.invocation))
363
+ return null;
364
+ const invocation = payload.invocation;
365
+ const server = typeof invocation.server === 'string' ? invocation.server : 'mcp';
366
+ const tool = typeof invocation.tool === 'string' ? invocation.tool : 'tool';
367
+ const args = isObject(invocation.arguments) ? invocation.arguments : {};
368
+ return {
369
+ toolName: `mcp__${server}__${tool}`,
370
+ toolInput: { ...args, server },
371
+ };
372
+ }
373
+ function normalizeCodexMcpToolOutput(result) {
374
+ if (!isObject(result))
375
+ return result === undefined ? '' : String(result);
376
+ const envelope = isObject(result.Ok) ? result.Ok : isObject(result.Err) ? result.Err : result;
377
+ if (Array.isArray(envelope.content)) {
378
+ const texts = envelope.content
379
+ .filter(isObject)
380
+ .map((entry) => typeof entry.text === 'string' ? entry.text : undefined)
381
+ .filter((text) => !!text);
382
+ if (texts.length > 0) {
383
+ const output = texts.join('\n');
384
+ return output.length > 4000 ? `${output.slice(0, 4000)}...` : output;
385
+ }
386
+ }
387
+ try {
388
+ const output = JSON.stringify(result);
389
+ return output.length > 4000 ? `${output.slice(0, 4000)}...` : output;
390
+ }
391
+ catch {
392
+ return String(result);
393
+ }
394
+ }
358
395
  function normalizeCodexEventFallbackText(eventType, payload) {
359
396
  let serialized = '';
360
397
  try {
@@ -1043,6 +1080,67 @@ function parseCodexEntryMessages(entry, messages, toolUseIdToName, patchDiffsByC
1043
1080
  if (payload.type === 'token_count') {
1044
1081
  return;
1045
1082
  }
1083
+ if (payload.type === 'mcp_tool_call_begin' || payload.type === 'mcp_tool_call_end') {
1084
+ const callId = typeof payload.call_id === 'string' ? payload.call_id : `${entry.timestamp}-mcp`;
1085
+ const normalized = normalizeCodexMcpToolCall(payload);
1086
+ const priorToolName = toolUseIdToName.get(callId);
1087
+ if (!normalized && !priorToolName)
1088
+ return;
1089
+ const toolName = normalized?.toolName ?? priorToolName;
1090
+ const alreadyStarted = toolUseIdToName.has(callId);
1091
+ if (!alreadyStarted && normalized) {
1092
+ toolUseIdToName.set(callId, toolName);
1093
+ messages.push({
1094
+ type: 'tool_use',
1095
+ content: JSON.stringify(normalized.toolInput, null, 2),
1096
+ timestamp: entry.timestamp,
1097
+ uuid: `${entry.timestamp}-tool-use-mcp`,
1098
+ toolName,
1099
+ toolInput: normalized.toolInput,
1100
+ toolUseId: callId,
1101
+ });
1102
+ }
1103
+ if (payload.type === 'mcp_tool_call_end') {
1104
+ messages.push({
1105
+ type: 'tool_result',
1106
+ content: normalizeCodexMcpToolOutput(payload.result),
1107
+ timestamp: entry.timestamp,
1108
+ uuid: `${entry.timestamp}-tool-result-mcp`,
1109
+ toolName,
1110
+ toolUseId: callId,
1111
+ });
1112
+ }
1113
+ return;
1114
+ }
1115
+ if (payload.type === 'web_search_begin' || payload.type === 'web_search_end') {
1116
+ const toolName = 'web_search';
1117
+ const toolInput = normalizeCodexWebSearchToolInput(payload);
1118
+ const callId = typeof payload.call_id === 'string' ? payload.call_id : `${entry.timestamp}-web-search`;
1119
+ const alreadyStarted = toolUseIdToName.has(callId);
1120
+ if (!alreadyStarted) {
1121
+ toolUseIdToName.set(callId, toolName);
1122
+ messages.push({
1123
+ type: 'tool_use',
1124
+ content: JSON.stringify(toolInput, null, 2),
1125
+ timestamp: entry.timestamp,
1126
+ uuid: `${entry.timestamp}-tool-use-web-search`,
1127
+ toolName,
1128
+ toolInput,
1129
+ toolUseId: callId,
1130
+ });
1131
+ }
1132
+ if (payload.type === 'web_search_end') {
1133
+ messages.push({
1134
+ type: 'tool_result',
1135
+ content: JSON.stringify(toolInput, null, 2),
1136
+ timestamp: entry.timestamp,
1137
+ uuid: `${entry.timestamp}-tool-result-web-search`,
1138
+ toolName,
1139
+ toolUseId: callId,
1140
+ });
1141
+ }
1142
+ return;
1143
+ }
1046
1144
  // agent_reasoning: Skip in history (consistent with Claude thinking behavior)
1047
1145
  if (payload.type === 'agent_reasoning') {
1048
1146
  return;
@@ -141,6 +141,9 @@ function parseResponsePayload(payload) {
141
141
  name: asString(payload.name),
142
142
  input: asString(payload.input),
143
143
  output: asString(payload.output),
144
+ query: asString(payload.query),
145
+ invocation: isObject(payload.invocation) ? payload.invocation : undefined,
146
+ result: payload.result,
144
147
  summary: parseSummaryArray(payload.summary),
145
148
  };
146
149
  }
@@ -150,6 +153,7 @@ function parseResponsePayload(payload) {
150
153
  */
151
154
  export class CodexJsonEventParser {
152
155
  activeToolByItemId = new Map();
156
+ activeEventToolCalls = new Map();
153
157
  lastAgentMessageText;
154
158
  lastErrorText;
155
159
  lastModelUsageSnapshot;
@@ -246,6 +250,16 @@ export class CodexJsonEventParser {
246
250
  };
247
251
  return [];
248
252
  }
253
+ // Newer Codex builds expose MCP and web activity as event_msg progress
254
+ // events instead of response_item entries. Normalize them into the same
255
+ // compact tool rows; otherwise Guake receives a multi-kilobyte raw JSON
256
+ // fallback for every completed call.
257
+ if (payloadType === 'mcp_tool_call_begin' || payloadType === 'mcp_tool_call_end') {
258
+ return this.parseMcpToolCallEvent(payload, payloadType === 'mcp_tool_call_end');
259
+ }
260
+ if (payloadType === 'web_search_begin' || payloadType === 'web_search_end') {
261
+ return this.parseWebSearchEvent(payload, payloadType === 'web_search_end');
262
+ }
249
263
  // agent_reasoning: Map to thinking event (stable uuid when item id present)
250
264
  if (payloadType === 'agent_reasoning') {
251
265
  const text = asString(payload.text);
@@ -300,6 +314,91 @@ export class CodexJsonEventParser {
300
314
  }
301
315
  return [this.buildUnknownEventFallback(`Unhandled Codex event_msg type: ${payloadType}`, payload)];
302
316
  }
317
+ parseMcpToolCallEvent(payload, completed) {
318
+ const invocation = payload.invocation;
319
+ const callId = payload.call_id;
320
+ const active = callId ? this.activeEventToolCalls.get(callId) : undefined;
321
+ if (!invocation && !active)
322
+ return [];
323
+ const server = invocation ? (asString(invocation.server) || 'mcp') : 'mcp';
324
+ const tool = invocation ? (asString(invocation.tool) || 'tool') : 'tool';
325
+ const toolName = active?.toolName ?? `mcp__${server}__${tool}`;
326
+ const args = invocation && isObject(invocation.arguments) ? invocation.arguments : {};
327
+ const toolInput = active?.toolInput ?? { ...args, server };
328
+ const hadStart = !!active;
329
+ if (!completed) {
330
+ if (callId)
331
+ this.activeEventToolCalls.set(callId, { toolName, toolInput });
332
+ return [{ type: 'tool_start', toolName, toolInput, uuid: callId, toolUseId: callId }];
333
+ }
334
+ if (callId)
335
+ this.activeEventToolCalls.delete(callId);
336
+ const result = {
337
+ type: 'tool_result',
338
+ toolName,
339
+ toolOutput: this.extractMcpToolOutput(payload.result),
340
+ uuid: callId,
341
+ toolUseId: callId,
342
+ };
343
+ return hadStart
344
+ ? [result]
345
+ : [{ type: 'tool_start', toolName, toolInput, uuid: callId, toolUseId: callId }, result];
346
+ }
347
+ parseWebSearchEvent(payload, completed) {
348
+ const callId = payload.call_id;
349
+ const toolName = 'web_search';
350
+ const active = callId ? this.activeEventToolCalls.get(callId) : undefined;
351
+ const currentInput = {
352
+ query: payload.query || payload.action?.query,
353
+ actionType: payload.action?.type,
354
+ actionQuery: payload.action?.query,
355
+ actionQueries: payload.action?.queries,
356
+ actionUrl: payload.action?.url,
357
+ };
358
+ const toolInput = active?.toolInput ?? currentInput;
359
+ const hadStart = !!active;
360
+ if (!completed) {
361
+ if (callId)
362
+ this.activeEventToolCalls.set(callId, { toolName, toolInput });
363
+ return [{ type: 'tool_start', toolName, toolInput, uuid: callId, toolUseId: callId }];
364
+ }
365
+ if (callId)
366
+ this.activeEventToolCalls.delete(callId);
367
+ const result = {
368
+ type: 'tool_result',
369
+ toolName,
370
+ toolOutput: JSON.stringify(toolInput),
371
+ uuid: callId,
372
+ toolUseId: callId,
373
+ };
374
+ return hadStart
375
+ ? [result]
376
+ : [{ type: 'tool_start', toolName, toolInput, uuid: callId, toolUseId: callId }, result];
377
+ }
378
+ extractMcpToolOutput(result) {
379
+ if (!isObject(result))
380
+ return result === undefined ? '' : String(result);
381
+ const envelope = isObject(result.Ok) ? result.Ok : isObject(result.Err) ? result.Err : result;
382
+ if (Array.isArray(envelope.content)) {
383
+ const texts = envelope.content
384
+ .filter(isObject)
385
+ .map((entry) => asString(entry.text))
386
+ .filter((text) => !!text);
387
+ if (texts.length > 0)
388
+ return this.truncateToolOutput(texts.join('\n'));
389
+ }
390
+ try {
391
+ return this.truncateToolOutput(JSON.stringify(result));
392
+ }
393
+ catch {
394
+ return String(result);
395
+ }
396
+ }
397
+ truncateToolOutput(output) {
398
+ return output.length > MAX_FALLBACK_EVENT_TEXT
399
+ ? `${output.slice(0, MAX_FALLBACK_EVENT_TEXT)}...`
400
+ : output;
401
+ }
303
402
  parseResponseItem(payload) {
304
403
  if (!payload?.type)
305
404
  return [];
@@ -19,6 +19,7 @@ import { initBackupService, shutdownBackupService } from './services/backup-serv
19
19
  import { initAutoUpdateService, shutdownAutoUpdateService } from './services/auto-update-service.js';
20
20
  import { initAttachmentJanitor, shutdownAttachmentJanitor } from './services/attachment-janitor.js';
21
21
  import { stopAllAgentTerminals, sweepAllAgentTtyds } from './services/agent-terminal-service.js';
22
+ import { initClaudeCredentialKeepAlive, shutdownClaudeCredentialKeepAlive, } from './services/claude-credentials-service.js';
22
23
  // Configuration
23
24
  const PORT = process.env.PORT || 6200;
24
25
  const HOST = process.env.HOST || (process.env.LISTEN_ALL_INTERFACES ? '::' : '127.0.0.1');
@@ -154,6 +155,8 @@ async function main() {
154
155
  initAutoUpdateService();
155
156
  // Start hourly sweeper for the trigger-attachment temp dir.
156
157
  initAttachmentJanitor();
158
+ // Keep every saved Claude OAuth account warm while Tide Commander is on.
159
+ initClaudeCredentialKeepAlive();
157
160
  logger.server.log(`Data directory: ${getDataDir()}`);
158
161
  logger.server.log(`Log file: ${getLogFilePath()}`);
159
162
  // Create Express app and HTTP server
@@ -218,6 +221,7 @@ async function main() {
218
221
  shutdownBackupService();
219
222
  shutdownAutoUpdateService();
220
223
  shutdownAttachmentJanitor();
224
+ shutdownClaudeCredentialKeepAlive();
221
225
  triggerService.shutdown();
222
226
  autoCollapseService.shutdown();
223
227
  workflowService.shutdown();
@@ -162,6 +162,13 @@ export function listClaudeCredentialProfiles() {
162
162
  const OAUTH_TOKEN_URL = 'https://console.anthropic.com/v1/oauth/token';
163
163
  const OAUTH_CLIENT_ID = '9d1c250a-e61b-44d9-88ed-5944d1962f5e';
164
164
  const OAUTH_REFRESH_TIMEOUT_MS = 10_000;
165
+ // Claude Code refreshes an access token when it is within five minutes of
166
+ // expiry. Tide goes a little earlier so it normally owns token rotation for
167
+ // every saved account (including the active one) and can persist the rotated
168
+ // grant to all duplicate profile files before a CLI process needs it.
169
+ const KEEP_ALIVE_INTERVAL_MS = 5 * 60 * 1000;
170
+ const KEEP_ALIVE_REFRESH_WINDOW_MS = 15 * 60 * 1000;
171
+ const KEEP_ALIVE_REFRESH_TOKEN_WINDOW_MS = 24 * 60 * 60 * 1000;
165
172
  const GRANT_USAGE_TTL_OK_MS = 2 * 60 * 1000;
166
173
  const GRANT_USAGE_TTL_ERR_MS = 45 * 1000;
167
174
  // After a failed refresh, leave the grant alone for a while — retrying a
@@ -183,22 +190,36 @@ function readProfileTokens(filePath) {
183
190
  expiresAt: typeof oauth?.expiresAt === 'number' ? oauth.expiresAt : null,
184
191
  refreshToken: typeof oauth?.refreshToken === 'string' && oauth.refreshToken ? oauth.refreshToken : null,
185
192
  refreshTokenExpiresAt: typeof oauth?.refreshTokenExpiresAt === 'number' ? oauth.refreshTokenExpiresAt : null,
193
+ scopes: Array.isArray(oauth?.scopes)
194
+ ? oauth.scopes.filter((scope) => typeof scope === 'string')
195
+ : [],
196
+ clientId: typeof oauth?.clientId === 'string' && oauth.clientId ? oauth.clientId : null,
186
197
  };
187
198
  }
188
199
  catch {
189
- return { accessToken: null, expiresAt: null, refreshToken: null, refreshTokenExpiresAt: null };
200
+ return {
201
+ accessToken: null,
202
+ expiresAt: null,
203
+ refreshToken: null,
204
+ refreshTokenExpiresAt: null,
205
+ scopes: [],
206
+ clientId: null,
207
+ };
190
208
  }
191
209
  }
192
210
  /** Refresh an OAuth grant the way the CLI does. Returns tokens or an error string. */
193
- async function refreshOAuthGrant(refreshToken) {
211
+ async function refreshOAuthGrant(tokens) {
212
+ if (!tokens.refreshToken)
213
+ return { error: 'No refresh token stored' };
194
214
  try {
195
215
  const response = await fetch(OAUTH_TOKEN_URL, {
196
216
  method: 'POST',
197
217
  headers: { 'Content-Type': 'application/json' },
198
218
  body: JSON.stringify({
199
219
  grant_type: 'refresh_token',
200
- refresh_token: refreshToken,
201
- client_id: OAUTH_CLIENT_ID,
220
+ refresh_token: tokens.refreshToken,
221
+ client_id: tokens.clientId ?? OAUTH_CLIENT_ID,
222
+ ...(tokens.scopes.length > 0 ? { scope: tokens.scopes.join(' ') } : {}),
202
223
  }),
203
224
  signal: AbortSignal.timeout(OAUTH_REFRESH_TIMEOUT_MS),
204
225
  });
@@ -210,10 +231,19 @@ async function refreshOAuthGrant(refreshToken) {
210
231
  if (!accessToken)
211
232
  return { error: 'Token refresh returned no access token' };
212
233
  const expiresIn = typeof body.expires_in === 'number' ? body.expires_in : 3600;
234
+ const refreshTokenExpiresIn = typeof body.refresh_token_expires_in === 'number'
235
+ ? body.refresh_token_expires_in
236
+ : null;
213
237
  return {
214
238
  accessToken,
215
239
  refreshToken: typeof body.refresh_token === 'string' && body.refresh_token ? body.refresh_token : null,
216
240
  expiresAt: Date.now() + expiresIn * 1000,
241
+ refreshTokenExpiresAt: refreshTokenExpiresIn == null
242
+ ? null
243
+ : Date.now() + refreshTokenExpiresIn * 1000,
244
+ scopes: typeof body.scope === 'string'
245
+ ? body.scope.split(' ').filter(Boolean)
246
+ : null,
217
247
  };
218
248
  }
219
249
  catch (err) {
@@ -229,9 +259,36 @@ function writeRefreshedTokens(filePath, refreshed) {
229
259
  oauth.expiresAt = refreshed.expiresAt;
230
260
  if (refreshed.refreshToken)
231
261
  oauth.refreshToken = refreshed.refreshToken;
262
+ if (refreshed.refreshTokenExpiresAt != null) {
263
+ oauth.refreshTokenExpiresAt = refreshed.refreshTokenExpiresAt;
264
+ }
265
+ if (refreshed.scopes)
266
+ oauth.scopes = refreshed.scopes;
232
267
  data.claudeAiOauth = oauth;
233
268
  writeCredentialsAtomic(filePath, JSON.stringify(data));
234
269
  }
270
+ /**
271
+ * Persist one rotated grant to all of its copies. If another process (usually
272
+ * Claude Code) changed any refresh token while our request was in flight, do
273
+ * not overwrite its newer credentials with our response.
274
+ */
275
+ function persistRefreshedGrant(group, refreshed) {
276
+ const expectedRefreshToken = group.tokens.refreshToken;
277
+ const changedPath = group.paths.find((filePath) => readProfileTokens(filePath).refreshToken !== expectedRefreshToken);
278
+ if (changedPath) {
279
+ log.warn(`Claude credentials changed during refresh; leaving newer on-disk grant untouched (${group.ids.join(', ')})`);
280
+ return false;
281
+ }
282
+ for (const filePath of group.paths) {
283
+ try {
284
+ writeRefreshedTokens(filePath, refreshed);
285
+ }
286
+ catch (err) {
287
+ log.warn(`Refreshed grant but failed to persist to ${filePath}: ${err}`);
288
+ }
289
+ }
290
+ return true;
291
+ }
235
292
  function grantCacheKey(token) {
236
293
  return crypto.createHash('sha256').update(token).digest('hex').slice(0, 12);
237
294
  }
@@ -267,7 +324,7 @@ function fetchUsageForGrant(cacheKey, group) {
267
324
  error = 'Session expired — sign in to this account again';
268
325
  }
269
326
  else {
270
- const refreshed = await refreshOAuthGrant(tokens.refreshToken);
327
+ const refreshed = await refreshOAuthGrant(tokens);
271
328
  if ('error' in refreshed) {
272
329
  accessToken = null;
273
330
  error = refreshed.error;
@@ -280,14 +337,7 @@ function fetchUsageForGrant(cacheKey, group) {
280
337
  grantUsageCache.set(cacheKey, entry);
281
338
  return entry;
282
339
  }
283
- for (const filePath of group.paths) {
284
- try {
285
- writeRefreshedTokens(filePath, refreshed);
286
- }
287
- catch (err) {
288
- log.warn(`Refreshed grant but failed to persist to ${filePath}: ${err}`);
289
- }
290
- }
340
+ persistRefreshedGrant(group, refreshed);
291
341
  log.log(`Refreshed dormant Claude credentials for profile(s): ${group.ids.join(', ')}`);
292
342
  accessToken = refreshed.accessToken;
293
343
  }
@@ -364,6 +414,115 @@ export async function getClaudeCredentialProfilesUsage() {
364
414
  }));
365
415
  return { usage };
366
416
  }
417
+ let keepAliveTimer = null;
418
+ let keepAliveInFlight = null;
419
+ const keepAliveFailureCooldown = new Map();
420
+ /** Run one keepalive pass. Exported to make the credential lifecycle testable. */
421
+ export function runClaudeCredentialKeepAliveNow() {
422
+ if (keepAliveInFlight)
423
+ return keepAliveInFlight;
424
+ keepAliveInFlight = (async () => {
425
+ const now = Date.now();
426
+ const list = listClaudeCredentialProfiles();
427
+ const candidates = [...(list.active ? [list.active] : []), ...list.profiles];
428
+ const groups = new Map();
429
+ for (const meta of candidates) {
430
+ if (!meta.valid)
431
+ continue;
432
+ const tokens = readProfileTokens(meta.path);
433
+ if (!tokens.refreshToken)
434
+ continue;
435
+ const key = grantCacheKey(tokens.refreshToken);
436
+ const existing = groups.get(key);
437
+ if (existing) {
438
+ existing.ids.push(meta.id);
439
+ if (!existing.paths.includes(meta.path))
440
+ existing.paths.push(meta.path);
441
+ existing.includesActive = existing.includesActive || meta.id === 'active';
442
+ // Copies can have different access-token expiries even while they still
443
+ // share a refresh grant. Refresh according to the freshest copy.
444
+ if ((tokens.expiresAt ?? 0) > (existing.tokens.expiresAt ?? 0))
445
+ existing.tokens = tokens;
446
+ }
447
+ else {
448
+ groups.set(key, {
449
+ ids: [meta.id],
450
+ paths: [meta.path],
451
+ includesActive: meta.id === 'active',
452
+ tokens,
453
+ });
454
+ }
455
+ }
456
+ const result = {
457
+ checkedGrants: groups.size,
458
+ refreshedGrants: 0,
459
+ skippedGrants: 0,
460
+ failedGrants: 0,
461
+ };
462
+ await Promise.all(Array.from(groups.entries()).map(async ([key, group]) => {
463
+ const { tokens } = group;
464
+ const refreshExpired = tokens.refreshTokenExpiresAt != null
465
+ && tokens.refreshTokenExpiresAt <= now;
466
+ const refreshDue = tokens.expiresAt == null
467
+ || tokens.expiresAt <= now + KEEP_ALIVE_REFRESH_WINDOW_MS
468
+ || (tokens.refreshTokenExpiresAt != null
469
+ && tokens.refreshTokenExpiresAt <= now + KEEP_ALIVE_REFRESH_TOKEN_WINDOW_MS);
470
+ const retryAfter = keepAliveFailureCooldown.get(key) ?? 0;
471
+ if (refreshExpired || !refreshDue || retryAfter > now) {
472
+ result.skippedGrants += 1;
473
+ return;
474
+ }
475
+ const refreshed = await refreshOAuthGrant(tokens);
476
+ if ('error' in refreshed) {
477
+ keepAliveFailureCooldown.set(key, Date.now() + GRANT_REFRESH_FAIL_COOLDOWN_MS);
478
+ result.failedGrants += 1;
479
+ log.warn(`Claude credential keepalive failed for ${group.ids.join(', ')}: ${refreshed.error}`);
480
+ return;
481
+ }
482
+ if (persistRefreshedGrant(group, refreshed)) {
483
+ result.refreshedGrants += 1;
484
+ keepAliveFailureCooldown.delete(key);
485
+ // Cached gauges contain the previous access token/fingerprint.
486
+ grantUsageCache.delete(key);
487
+ resetClaudeRateLimitCache();
488
+ log.log(`Kept Claude account session alive: ${group.ids.join(', ')}`);
489
+ }
490
+ else {
491
+ result.skippedGrants += 1;
492
+ }
493
+ }));
494
+ return result;
495
+ })().finally(() => {
496
+ keepAliveInFlight = null;
497
+ });
498
+ return keepAliveInFlight;
499
+ }
500
+ /** Start refreshing saved Claude account sessions for the server lifetime. */
501
+ export function initClaudeCredentialKeepAlive() {
502
+ if (keepAliveTimer || process.env.CLAUDE_CREDENTIAL_KEEPALIVE === '0')
503
+ return;
504
+ void runClaudeCredentialKeepAliveNow().catch((err) => {
505
+ log.warn(`Initial Claude credential keepalive failed: ${err}`);
506
+ });
507
+ keepAliveTimer = setInterval(() => {
508
+ void runClaudeCredentialKeepAliveNow().catch((err) => {
509
+ log.warn(`Claude credential keepalive pass failed: ${err}`);
510
+ });
511
+ }, KEEP_ALIVE_INTERVAL_MS);
512
+ keepAliveTimer.unref?.();
513
+ log.log('Claude credential keepalive started');
514
+ }
515
+ /** Stop the keepalive scheduler during graceful shutdown. */
516
+ export function shutdownClaudeCredentialKeepAlive() {
517
+ if (keepAliveTimer)
518
+ clearInterval(keepAliveTimer);
519
+ keepAliveTimer = null;
520
+ }
521
+ export function resetClaudeCredentialKeepAliveForTests() {
522
+ shutdownClaudeCredentialKeepAlive();
523
+ keepAliveFailureCooldown.clear();
524
+ keepAliveInFlight = null;
525
+ }
367
526
  function assertValidName(name) {
368
527
  if (!isValidProfileName(name)) {
369
528
  throw new Error(`Invalid profile name "${name}". Use 1–64 chars: letters, digits, dot, underscore, hyphen; must start with alphanumeric.`);
@@ -328,12 +328,16 @@ export async function fetchClaudeRateLimitsForToken(accessToken) {
328
328
  };
329
329
  }
330
330
  const body = await response.json();
331
+ // Claude Code 2.1.207 labels this allowance "Fable", although the live
332
+ // endpoint still exposes it under the legacy seven_day_sonnet key.
333
+ const sevenDayFable = parseRateLimitWindow(body.seven_day_fable ?? body.seven_day_sonnet);
331
334
  return {
332
335
  rateLimits: {
333
336
  fiveHour: parseRateLimitWindow(body.five_hour),
334
337
  sevenDay: parseRateLimitWindow(body.seven_day),
335
338
  sevenDayOpus: parseRateLimitWindow(body.seven_day_opus),
336
- sevenDaySonnet: parseRateLimitWindow(body.seven_day_sonnet),
339
+ sevenDayFable,
340
+ sevenDaySonnet: sevenDayFable,
337
341
  },
338
342
  error: null,
339
343
  status: response.status,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tide-commander",
3
- "version": "1.160.0",
3
+ "version": "1.161.0",
4
4
  "description": "Visual multi-agent orchestrator and manager for Claude Code with 3D/2D interface",
5
5
  "repository": {
6
6
  "type": "git",