tide-commander 1.160.0 → 1.162.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 (57) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/dist/assets/{BossLogsModal-COg0RBw_.js → BossLogsModal-DrYF13xp.js} +1 -1
  3. package/dist/assets/{BossSpawnModal-Bvkk2uFi.js → BossSpawnModal-2ZgbW8Iy.js} +1 -1
  4. package/dist/assets/{ControlsModal-BWcWGs2x.js → ControlsModal-CRdNNTEo.js} +1 -1
  5. package/dist/assets/{DockerLogsModal-C4tJ59Pf.js → DockerLogsModal-BGPZP9_V.js} +1 -1
  6. package/dist/assets/{EmbeddedEditor-DiDm5EvF.js → EmbeddedEditor-B99bT-bl.js} +1 -1
  7. package/dist/assets/{GmailOAuthSetup-GkYs6wUs.js → GmailOAuthSetup-DmMCxdUW.js} +1 -1
  8. package/dist/assets/{GoogleOAuthSetup-KKwp2gEQ.js → GoogleOAuthSetup-BZqit-mW.js} +1 -1
  9. package/dist/assets/{IframeModal-D0iTkxZB.js → IframeModal-vMY7XxHP.js} +1 -1
  10. package/dist/assets/{IntegrationsPanel-CCKoizJa.js → IntegrationsPanel-DVmqPRTs.js} +2 -2
  11. package/dist/assets/{LogViewerModal-CSGEkyhZ.js → LogViewerModal-rYC6IVPV.js} +1 -1
  12. package/dist/assets/{MonitoringModal-Be6uEd7K.js → MonitoringModal-C-QXpZkE.js} +1 -1
  13. package/dist/assets/{PM2LogsModal-DZG7Tcw7.js → PM2LogsModal-C3A_ohFv.js} +1 -1
  14. package/dist/assets/{RestoreArchivedAreaModal-CigBbBB_.js → RestoreArchivedAreaModal-Dx94NzSz.js} +1 -1
  15. package/dist/assets/{Scene2DCanvas-sJig3vJ4.js → Scene2DCanvas-BxOqJ0IB.js} +1 -1
  16. package/dist/assets/{SceneManager-BG_0UVJc.js → SceneManager-CxcXfq_u.js} +1 -1
  17. package/dist/assets/{SkillsPanel-BnAf8uyn.js → SkillsPanel-Fu5sfG7Q.js} +1 -1
  18. package/dist/assets/{SlackMultiInstanceSetup-DGuexHRz.js → SlackMultiInstanceSetup-jnGMesoq.js} +1 -1
  19. package/dist/assets/{SpawnModal-FjDJZlIm.js → SpawnModal-CGYg4uTC.js} +1 -1
  20. package/dist/assets/{StatisticsModal-CbbdwSF0.js → StatisticsModal-BOV6Juts.js} +1 -1
  21. package/dist/assets/{SubordinateAssignmentModal-Dn2uuOcT.js → SubordinateAssignmentModal-Bhgj9FvT.js} +1 -1
  22. package/dist/assets/{TriggerManagerPanel-Cj9pqf-r.js → TriggerManagerPanel-Qkw4ajmL.js} +1 -1
  23. package/dist/assets/{WorkflowEditorPanel-nucrx_0v.js → WorkflowEditorPanel-DTIBRGz4.js} +1 -1
  24. package/dist/assets/{area-logos-C93KZk3h.js → area-logos-uM2FM0I2.js} +1 -1
  25. package/dist/assets/{index-Gjc7NXlZ.js → index-3y5Ajiyr.js} +1 -1
  26. package/dist/assets/{index-iX9jPnEp.js → index-5tz2e3op.js} +1 -1
  27. package/dist/assets/{index-ojmo-Cva.js → index-B03BKiDN.js} +1 -1
  28. package/dist/assets/index-BFJWNGpT.js +19 -0
  29. package/dist/assets/{index-BkbKu66S.js → index-BvD1v4JH.js} +1 -1
  30. package/dist/assets/{index-D7YoMagO.js → index-BvOgm4yp.js} +1 -1
  31. package/dist/assets/{index-CmmI2kZf.js → index-CpQeaxZ7.js} +2 -2
  32. package/dist/assets/index-CrBXvs1w.css +1 -0
  33. package/dist/assets/{index-fh53j_4E.js → index-DzQxMm67.js} +1 -1
  34. package/dist/assets/index-cA0kPNUq.js +2 -0
  35. package/dist/assets/{main-BZcOdtO_.css → main-B-jhtDyZ.css} +1 -1
  36. package/dist/assets/main-rX2ghMIV.js +297 -0
  37. package/dist/assets/{web-8gl2noUE.js → web-ClQyiX7L.js} +1 -1
  38. package/dist/assets/{web-M9cLwTrs.js → web-NCIGsXXA.js} +1 -1
  39. package/dist/assets/{web-D0uiHhnA.js → web-ml-7D2g7.js} +1 -1
  40. package/dist/build-info.json +1 -1
  41. package/dist/index.html +2 -2
  42. package/dist/locales/en/terminal.json +4 -0
  43. package/dist/locales/es/terminal.json +1 -0
  44. package/dist/src/packages/server/claude/backend.js +4 -0
  45. package/dist/src/packages/server/claude/session-loader.js +116 -0
  46. package/dist/src/packages/server/codex/generated-image.js +35 -0
  47. package/dist/src/packages/server/codex/json-event-parser.js +122 -0
  48. package/dist/src/packages/server/index.js +4 -0
  49. package/dist/src/packages/server/routes/agents.js +9 -6
  50. package/dist/src/packages/server/services/claude-credentials-service.js +172 -13
  51. package/dist/src/packages/server/services/claude-usage-service.js +5 -1
  52. package/dist/src/packages/server/services/codex-usage-service.js +93 -0
  53. package/package.json +1 -1
  54. package/dist/assets/index-BIxFr4P2.css +0 -1
  55. package/dist/assets/index-DIOhr3im.js +0 -19
  56. package/dist/assets/index-DqM0QltS.js +0 -2
  57. package/dist/assets/main-BFW0rGXU.js +0 -297
@@ -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-rX2ghMIV.js";import{ImpactStyle as i,NotificationType as r}from"./index-CpQeaxZ7.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 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-rX2ghMIV.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 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-rX2ghMIV.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.162.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-rX2ghMIV.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-B-jhtDyZ.css">
29
29
  </head>
30
30
  <body>
31
31
  <div id="app"></div>
@@ -1270,6 +1270,7 @@
1270
1270
  "usage": {
1271
1271
  "title": "Claude Usage",
1272
1272
  "titleGrok": "Grok Usage",
1273
+ "titleCodex": "Codex Usage",
1273
1274
  "session": "Session",
1274
1275
  "tokensUsed": "Tokens used",
1275
1276
  "taskCount": "Tasks sent",
@@ -1285,8 +1286,10 @@
1285
1286
  "currentSession": "Current session",
1286
1287
  "currentWeekAll": "Current week (all models)",
1287
1288
  "currentWeekOpus": "Current week (Opus only)",
1289
+ "currentWeekFable": "Current week (Fable)",
1288
1290
  "currentWeekSonnet": "Current week (Sonnet only)",
1289
1291
  "weeklyLimit": "Weekly limit",
1292
+ "dailyLimit": "Daily limit",
1290
1293
  "monthlyLimit": "Monthly limit",
1291
1294
  "periodLimit": "Plan period limit",
1292
1295
  "onDemandLimit": "Pay-as-you-go limit",
@@ -1327,6 +1330,7 @@
1327
1330
  "gaugeSession": "Session",
1328
1331
  "gaugeWeek": "Week",
1329
1332
  "gaugeWeekOpus": "Opus wk",
1333
+ "gaugeWeekFable": "Fable wk",
1330
1334
  "gaugeNoSession": "no active session",
1331
1335
  "providerTitle": "{{provider}} Accounts",
1332
1336
  "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,10 @@ export class ClaudeBackend {
348
348
  toolName,
349
349
  toolOutput: content,
350
350
  toolUseId: block.tool_use_id, // Preserve for subagent correlation
351
+ // The matching tool_start uses the tool_use id as its live-output
352
+ // uuid. Reuse it for the result so clients can enrich that card in
353
+ // place instead of waiting for a session-history reconstruction.
354
+ uuid: block.tool_use_id,
351
355
  };
352
356
  // Extract subagent stats from Task/Agent tool completion metadata
353
357
  if ((toolName === 'Task' || toolName === 'Agent') && event.tool_use_result) {
@@ -11,6 +11,7 @@ import * as os from 'os';
11
11
  import * as readline from 'readline';
12
12
  import Database from 'better-sqlite3';
13
13
  import { createLogger } from '../utils/logger.js';
14
+ import { materializeCodexGeneratedImage } from '../codex/generated-image.js';
14
15
  const log = createLogger('Session');
15
16
  // Claude's project directory
16
17
  const CLAUDE_DIR = path.join(os.homedir(), '.claude');
@@ -348,6 +349,9 @@ function normalizeCodexWebSearchToolInput(payload) {
348
349
  ? actionQueriesRaw
349
350
  : undefined;
350
351
  return {
352
+ query: typeof payload.query === 'string'
353
+ ? payload.query
354
+ : typeof action.query === 'string' ? action.query : undefined,
351
355
  actionType: typeof action.type === 'string' ? action.type : undefined,
352
356
  actionQuery: typeof action.query === 'string' ? action.query : undefined,
353
357
  actionQueries,
@@ -355,6 +359,40 @@ function normalizeCodexWebSearchToolInput(payload) {
355
359
  status: typeof payload.status === 'string' ? payload.status : undefined,
356
360
  };
357
361
  }
362
+ function normalizeCodexMcpToolCall(payload) {
363
+ if (!isObject(payload.invocation))
364
+ return null;
365
+ const invocation = payload.invocation;
366
+ const server = typeof invocation.server === 'string' ? invocation.server : 'mcp';
367
+ const tool = typeof invocation.tool === 'string' ? invocation.tool : 'tool';
368
+ const args = isObject(invocation.arguments) ? invocation.arguments : {};
369
+ return {
370
+ toolName: `mcp__${server}__${tool}`,
371
+ toolInput: { ...args, server },
372
+ };
373
+ }
374
+ function normalizeCodexMcpToolOutput(result) {
375
+ if (!isObject(result))
376
+ return result === undefined ? '' : String(result);
377
+ const envelope = isObject(result.Ok) ? result.Ok : isObject(result.Err) ? result.Err : result;
378
+ if (Array.isArray(envelope.content)) {
379
+ const texts = envelope.content
380
+ .filter(isObject)
381
+ .map((entry) => typeof entry.text === 'string' ? entry.text : undefined)
382
+ .filter((text) => !!text);
383
+ if (texts.length > 0) {
384
+ const output = texts.join('\n');
385
+ return output.length > 4000 ? `${output.slice(0, 4000)}...` : output;
386
+ }
387
+ }
388
+ try {
389
+ const output = JSON.stringify(result);
390
+ return output.length > 4000 ? `${output.slice(0, 4000)}...` : output;
391
+ }
392
+ catch {
393
+ return String(result);
394
+ }
395
+ }
358
396
  function normalizeCodexEventFallbackText(eventType, payload) {
359
397
  let serialized = '';
360
398
  try {
@@ -1043,6 +1081,84 @@ function parseCodexEntryMessages(entry, messages, toolUseIdToName, patchDiffsByC
1043
1081
  if (payload.type === 'token_count') {
1044
1082
  return;
1045
1083
  }
1084
+ if (payload.type === 'mcp_tool_call_begin' || payload.type === 'mcp_tool_call_end') {
1085
+ const callId = typeof payload.call_id === 'string' ? payload.call_id : `${entry.timestamp}-mcp`;
1086
+ const normalized = normalizeCodexMcpToolCall(payload);
1087
+ const priorToolName = toolUseIdToName.get(callId);
1088
+ if (!normalized && !priorToolName)
1089
+ return;
1090
+ const toolName = normalized?.toolName ?? priorToolName;
1091
+ const alreadyStarted = toolUseIdToName.has(callId);
1092
+ if (!alreadyStarted && normalized) {
1093
+ toolUseIdToName.set(callId, toolName);
1094
+ messages.push({
1095
+ type: 'tool_use',
1096
+ content: JSON.stringify(normalized.toolInput, null, 2),
1097
+ timestamp: entry.timestamp,
1098
+ uuid: `${entry.timestamp}-tool-use-mcp`,
1099
+ toolName,
1100
+ toolInput: normalized.toolInput,
1101
+ toolUseId: callId,
1102
+ });
1103
+ }
1104
+ if (payload.type === 'mcp_tool_call_end') {
1105
+ messages.push({
1106
+ type: 'tool_result',
1107
+ content: normalizeCodexMcpToolOutput(payload.result),
1108
+ timestamp: entry.timestamp,
1109
+ uuid: `${entry.timestamp}-tool-result-mcp`,
1110
+ toolName,
1111
+ toolUseId: callId,
1112
+ });
1113
+ }
1114
+ return;
1115
+ }
1116
+ if (payload.type === 'web_search_begin' || payload.type === 'web_search_end') {
1117
+ const toolName = 'web_search';
1118
+ const toolInput = normalizeCodexWebSearchToolInput(payload);
1119
+ const callId = typeof payload.call_id === 'string' ? payload.call_id : `${entry.timestamp}-web-search`;
1120
+ const alreadyStarted = toolUseIdToName.has(callId);
1121
+ if (!alreadyStarted) {
1122
+ toolUseIdToName.set(callId, toolName);
1123
+ messages.push({
1124
+ type: 'tool_use',
1125
+ content: JSON.stringify(toolInput, null, 2),
1126
+ timestamp: entry.timestamp,
1127
+ uuid: `${entry.timestamp}-tool-use-web-search`,
1128
+ toolName,
1129
+ toolInput,
1130
+ toolUseId: callId,
1131
+ });
1132
+ }
1133
+ if (payload.type === 'web_search_end') {
1134
+ messages.push({
1135
+ type: 'tool_result',
1136
+ content: JSON.stringify(toolInput, null, 2),
1137
+ timestamp: entry.timestamp,
1138
+ uuid: `${entry.timestamp}-tool-result-web-search`,
1139
+ toolName,
1140
+ toolUseId: callId,
1141
+ });
1142
+ }
1143
+ return;
1144
+ }
1145
+ if (payload.type === 'image_generation_begin') {
1146
+ return;
1147
+ }
1148
+ if (payload.type === 'image_generation_end') {
1149
+ const imagePath = materializeCodexGeneratedImage(payload.result);
1150
+ messages.push({
1151
+ type: 'assistant',
1152
+ content: imagePath
1153
+ ? `[Image: ${imagePath}]`
1154
+ : '[codex-event] Image generation completed, but the PNG result was invalid.',
1155
+ timestamp: entry.timestamp,
1156
+ uuid: typeof payload.call_id === 'string'
1157
+ ? payload.call_id
1158
+ : `${entry.timestamp}-assistant-generated-image`,
1159
+ });
1160
+ return;
1161
+ }
1046
1162
  // agent_reasoning: Skip in history (consistent with Claude thinking behavior)
1047
1163
  if (payload.type === 'agent_reasoning') {
1048
1164
  return;
@@ -0,0 +1,35 @@
1
+ import { createHash } from 'crypto';
2
+ import * as fs from 'fs';
3
+ import * as os from 'os';
4
+ import * as path from 'path';
5
+ const GENERATED_IMAGES_DIR = path.join(os.tmpdir(), 'tide-commander-uploads');
6
+ const MAX_GENERATED_IMAGE_BYTES = 50 * 1024 * 1024;
7
+ const PNG_SIGNATURE = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
8
+ /**
9
+ * Materialize Codex's image_generation_end base64 result in the directory
10
+ * already exposed by Tide Commander at /uploads.
11
+ */
12
+ export function materializeCodexGeneratedImage(result) {
13
+ if (typeof result !== 'string')
14
+ return null;
15
+ const encoded = result.startsWith('data:image/png;base64,')
16
+ ? result.slice('data:image/png;base64,'.length)
17
+ : result;
18
+ if (!encoded || encoded.length > Math.ceil(MAX_GENERATED_IMAGE_BYTES * 4 / 3) + 4)
19
+ return null;
20
+ if (!/^[A-Za-z0-9+/]*={0,2}$/.test(encoded))
21
+ return null;
22
+ const image = Buffer.from(encoded, 'base64');
23
+ if (image.length === 0 ||
24
+ image.length > MAX_GENERATED_IMAGE_BYTES ||
25
+ image.length < PNG_SIGNATURE.length ||
26
+ !image.subarray(0, PNG_SIGNATURE.length).equals(PNG_SIGNATURE)) {
27
+ return null;
28
+ }
29
+ const digest = createHash('sha256').update(image).digest('hex').slice(0, 16);
30
+ const imagePath = path.join(GENERATED_IMAGES_DIR, `codex-generated-${digest}.png`);
31
+ fs.mkdirSync(GENERATED_IMAGES_DIR, { recursive: true });
32
+ if (!fs.existsSync(imagePath))
33
+ fs.writeFileSync(imagePath, image, { mode: 0o600 });
34
+ return imagePath;
35
+ }
@@ -2,6 +2,7 @@ import { execFileSync } from 'child_process';
2
2
  import * as fs from 'fs';
3
3
  import * as path from 'path';
4
4
  import { createLogger } from '../utils/logger.js';
5
+ import { materializeCodexGeneratedImage } from './generated-image.js';
5
6
  const log = createLogger('CodexParser');
6
7
  let hasLoggedTurnAbortedLiveWarning = false;
7
8
  const MAX_DIFF_FILE_BYTES = 256 * 1024;
@@ -141,6 +142,9 @@ function parseResponsePayload(payload) {
141
142
  name: asString(payload.name),
142
143
  input: asString(payload.input),
143
144
  output: asString(payload.output),
145
+ query: asString(payload.query),
146
+ invocation: isObject(payload.invocation) ? payload.invocation : undefined,
147
+ result: payload.result,
144
148
  summary: parseSummaryArray(payload.summary),
145
149
  };
146
150
  }
@@ -150,6 +154,7 @@ function parseResponsePayload(payload) {
150
154
  */
151
155
  export class CodexJsonEventParser {
152
156
  activeToolByItemId = new Map();
157
+ activeEventToolCalls = new Map();
153
158
  lastAgentMessageText;
154
159
  lastErrorText;
155
160
  lastModelUsageSnapshot;
@@ -246,6 +251,38 @@ export class CodexJsonEventParser {
246
251
  };
247
252
  return [];
248
253
  }
254
+ // Newer Codex builds expose MCP and web activity as event_msg progress
255
+ // events instead of response_item entries. Normalize them into the same
256
+ // compact tool rows; otherwise Guake receives a multi-kilobyte raw JSON
257
+ // fallback for every completed call.
258
+ if (payloadType === 'mcp_tool_call_begin' || payloadType === 'mcp_tool_call_end') {
259
+ return this.parseMcpToolCallEvent(payload, payloadType === 'mcp_tool_call_end');
260
+ }
261
+ if (payloadType === 'web_search_begin' || payloadType === 'web_search_end') {
262
+ return this.parseWebSearchEvent(payload, payloadType === 'web_search_end');
263
+ }
264
+ // Codex returns generated PNGs inline as base64. Persist the bytes in the
265
+ // existing uploads directory and let Guake's normal image-reference
266
+ // renderer show a clickable thumbnail instead of dumping the JSON/base64.
267
+ if (payloadType === 'image_generation_begin') {
268
+ return [];
269
+ }
270
+ if (payloadType === 'image_generation_end') {
271
+ const imagePath = materializeCodexGeneratedImage(payload.result);
272
+ if (!imagePath) {
273
+ return [{
274
+ type: 'text',
275
+ text: '[codex-event] Image generation completed, but the PNG result was invalid.',
276
+ isStreaming: false,
277
+ }];
278
+ }
279
+ return [{
280
+ type: 'text',
281
+ text: `[Image: ${imagePath}]`,
282
+ isStreaming: false,
283
+ uuid: asString(payload.call_id) || undefined,
284
+ }];
285
+ }
249
286
  // agent_reasoning: Map to thinking event (stable uuid when item id present)
250
287
  if (payloadType === 'agent_reasoning') {
251
288
  const text = asString(payload.text);
@@ -300,6 +337,91 @@ export class CodexJsonEventParser {
300
337
  }
301
338
  return [this.buildUnknownEventFallback(`Unhandled Codex event_msg type: ${payloadType}`, payload)];
302
339
  }
340
+ parseMcpToolCallEvent(payload, completed) {
341
+ const invocation = payload.invocation;
342
+ const callId = payload.call_id;
343
+ const active = callId ? this.activeEventToolCalls.get(callId) : undefined;
344
+ if (!invocation && !active)
345
+ return [];
346
+ const server = invocation ? (asString(invocation.server) || 'mcp') : 'mcp';
347
+ const tool = invocation ? (asString(invocation.tool) || 'tool') : 'tool';
348
+ const toolName = active?.toolName ?? `mcp__${server}__${tool}`;
349
+ const args = invocation && isObject(invocation.arguments) ? invocation.arguments : {};
350
+ const toolInput = active?.toolInput ?? { ...args, server };
351
+ const hadStart = !!active;
352
+ if (!completed) {
353
+ if (callId)
354
+ this.activeEventToolCalls.set(callId, { toolName, toolInput });
355
+ return [{ type: 'tool_start', toolName, toolInput, uuid: callId, toolUseId: callId }];
356
+ }
357
+ if (callId)
358
+ this.activeEventToolCalls.delete(callId);
359
+ const result = {
360
+ type: 'tool_result',
361
+ toolName,
362
+ toolOutput: this.extractMcpToolOutput(payload.result),
363
+ uuid: callId,
364
+ toolUseId: callId,
365
+ };
366
+ return hadStart
367
+ ? [result]
368
+ : [{ type: 'tool_start', toolName, toolInput, uuid: callId, toolUseId: callId }, result];
369
+ }
370
+ parseWebSearchEvent(payload, completed) {
371
+ const callId = payload.call_id;
372
+ const toolName = 'web_search';
373
+ const active = callId ? this.activeEventToolCalls.get(callId) : undefined;
374
+ const currentInput = {
375
+ query: payload.query || payload.action?.query,
376
+ actionType: payload.action?.type,
377
+ actionQuery: payload.action?.query,
378
+ actionQueries: payload.action?.queries,
379
+ actionUrl: payload.action?.url,
380
+ };
381
+ const toolInput = active?.toolInput ?? currentInput;
382
+ const hadStart = !!active;
383
+ if (!completed) {
384
+ if (callId)
385
+ this.activeEventToolCalls.set(callId, { toolName, toolInput });
386
+ return [{ type: 'tool_start', toolName, toolInput, uuid: callId, toolUseId: callId }];
387
+ }
388
+ if (callId)
389
+ this.activeEventToolCalls.delete(callId);
390
+ const result = {
391
+ type: 'tool_result',
392
+ toolName,
393
+ toolOutput: JSON.stringify(toolInput),
394
+ uuid: callId,
395
+ toolUseId: callId,
396
+ };
397
+ return hadStart
398
+ ? [result]
399
+ : [{ type: 'tool_start', toolName, toolInput, uuid: callId, toolUseId: callId }, result];
400
+ }
401
+ extractMcpToolOutput(result) {
402
+ if (!isObject(result))
403
+ return result === undefined ? '' : String(result);
404
+ const envelope = isObject(result.Ok) ? result.Ok : isObject(result.Err) ? result.Err : result;
405
+ if (Array.isArray(envelope.content)) {
406
+ const texts = envelope.content
407
+ .filter(isObject)
408
+ .map((entry) => asString(entry.text))
409
+ .filter((text) => !!text);
410
+ if (texts.length > 0)
411
+ return this.truncateToolOutput(texts.join('\n'));
412
+ }
413
+ try {
414
+ return this.truncateToolOutput(JSON.stringify(result));
415
+ }
416
+ catch {
417
+ return String(result);
418
+ }
419
+ }
420
+ truncateToolOutput(output) {
421
+ return output.length > MAX_FALLBACK_EVENT_TEXT
422
+ ? `${output.slice(0, MAX_FALLBACK_EVENT_TEXT)}...`
423
+ : output;
424
+ }
303
425
  parseResponseItem(payload) {
304
426
  if (!payload?.type)
305
427
  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();
@@ -23,6 +23,7 @@ import { markInstructionsDirtyForAll } from '../services/instruction-refresh.js'
23
23
  import { startAgentTerminal, stopAgentTerminal } from '../services/agent-terminal-service.js';
24
24
  import { buildClaudeUsageByAgentSummary, buildClaudeUsageByDaySummary, buildClaudeUsageSnapshot } from '../services/claude-usage-service.js';
25
25
  import { buildGrokUsageSnapshot } from '../services/grok-usage-service.js';
26
+ import { buildCodexUsageSnapshot } from '../services/codex-usage-service.js';
26
27
  import { getBackupStatus, setBackupEnabled } from '../services/backup-service.js';
27
28
  const log = createLogger('Routes');
28
29
  const router = Router();
@@ -949,7 +950,7 @@ router.delete('/:id', (req, res) => {
949
950
  }
950
951
  res.status(204).end();
951
952
  });
952
- // GET /api/agents/:id/usage - Claude or Grok usage snapshot for a single agent
953
+ // GET /api/agents/:id/usage - provider usage snapshot for a single agent
953
954
  //
954
955
  // Claude: local agent stats + Anthropic OAuth rate-limit gauges (CLI `/usage`).
955
956
  // Grok: local agent stats + CLI chat-proxy billing/credit gauges (CLI `/usage`).
@@ -960,17 +961,19 @@ router.get('/:id/usage', async (req, res) => {
960
961
  return;
961
962
  }
962
963
  const provider = agent.provider ?? 'claude';
963
- if (provider !== 'claude' && provider !== 'grok') {
964
+ if (provider !== 'claude' && provider !== 'grok' && provider !== 'codex') {
964
965
  res.status(400).json({
965
- error: 'Usage data is only available for Claude and Grok agents',
966
+ error: 'Usage data is only available for Claude, Codex, and Grok agents',
966
967
  provider,
967
968
  });
968
969
  return;
969
970
  }
970
971
  try {
971
- const snapshot = provider === 'grok'
972
- ? await buildGrokUsageSnapshot(agent)
973
- : await buildClaudeUsageSnapshot(agent);
972
+ const snapshot = provider === 'codex'
973
+ ? await buildCodexUsageSnapshot(agent)
974
+ : provider === 'grok'
975
+ ? await buildGrokUsageSnapshot(agent)
976
+ : await buildClaudeUsageSnapshot(agent);
974
977
  res.json(snapshot);
975
978
  }
976
979
  catch (err) {