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.
- package/CHANGELOG.md +12 -0
- package/dist/assets/{BossLogsModal-COg0RBw_.js → BossLogsModal-ByN3qs9-.js} +1 -1
- package/dist/assets/{BossSpawnModal-Bvkk2uFi.js → BossSpawnModal-CNlqXvK9.js} +1 -1
- package/dist/assets/{ControlsModal-BWcWGs2x.js → ControlsModal-BxKH1QLw.js} +1 -1
- package/dist/assets/{DockerLogsModal-C4tJ59Pf.js → DockerLogsModal-BPGru2EX.js} +1 -1
- package/dist/assets/{EmbeddedEditor-DiDm5EvF.js → EmbeddedEditor-kSj9yTQI.js} +1 -1
- package/dist/assets/{GmailOAuthSetup-GkYs6wUs.js → GmailOAuthSetup-aaL2rsLN.js} +1 -1
- package/dist/assets/{GoogleOAuthSetup-KKwp2gEQ.js → GoogleOAuthSetup-Bc61FQ2H.js} +1 -1
- package/dist/assets/{IframeModal-D0iTkxZB.js → IframeModal-DvM559FE.js} +1 -1
- package/dist/assets/{IntegrationsPanel-CCKoizJa.js → IntegrationsPanel-4_733lDP.js} +2 -2
- package/dist/assets/{LogViewerModal-CSGEkyhZ.js → LogViewerModal-BjC13hIK.js} +1 -1
- package/dist/assets/{MonitoringModal-Be6uEd7K.js → MonitoringModal-Xjkid3A5.js} +1 -1
- package/dist/assets/{PM2LogsModal-DZG7Tcw7.js → PM2LogsModal-DpbDESnu.js} +1 -1
- package/dist/assets/{RestoreArchivedAreaModal-CigBbBB_.js → RestoreArchivedAreaModal-B-9zHb3p.js} +1 -1
- package/dist/assets/{Scene2DCanvas-sJig3vJ4.js → Scene2DCanvas-DQdSAptp.js} +1 -1
- package/dist/assets/{SceneManager-BG_0UVJc.js → SceneManager-Bn-9k6mM.js} +1 -1
- package/dist/assets/{SkillsPanel-BnAf8uyn.js → SkillsPanel-Bk3rbNQB.js} +1 -1
- package/dist/assets/{SlackMultiInstanceSetup-DGuexHRz.js → SlackMultiInstanceSetup-CfhQ10Um.js} +1 -1
- package/dist/assets/{SpawnModal-FjDJZlIm.js → SpawnModal-DfT7bzNo.js} +1 -1
- package/dist/assets/{StatisticsModal-CbbdwSF0.js → StatisticsModal-DFDFE58F.js} +1 -1
- package/dist/assets/{SubordinateAssignmentModal-Dn2uuOcT.js → SubordinateAssignmentModal-hqH3o5Lp.js} +1 -1
- package/dist/assets/{TriggerManagerPanel-Cj9pqf-r.js → TriggerManagerPanel-svSECUjd.js} +1 -1
- package/dist/assets/{WorkflowEditorPanel-nucrx_0v.js → WorkflowEditorPanel-CIqg8BLi.js} +1 -1
- package/dist/assets/{area-logos-C93KZk3h.js → area-logos-B0efSjrz.js} +1 -1
- package/dist/assets/{index-CmmI2kZf.js → index-B8R_gEnb.js} +2 -2
- package/dist/assets/{index-Gjc7NXlZ.js → index-BeQoCc1W.js} +1 -1
- package/dist/assets/{index-ojmo-Cva.js → index-CI4TPz4h.js} +1 -1
- package/dist/assets/index-CMI4WoS7.js +2 -0
- package/dist/assets/index-CPVGsE7g.js +19 -0
- package/dist/assets/{index-BkbKu66S.js → index-CmiQbHNF.js} +1 -1
- package/dist/assets/{index-D7YoMagO.js → index-Db0U-21f.js} +1 -1
- package/dist/assets/{index-fh53j_4E.js → index-F4gvAXEg.js} +1 -1
- package/dist/assets/{index-iX9jPnEp.js → index-hzbuivYC.js} +1 -1
- package/dist/assets/{main-BZcOdtO_.css → main-BX-OUL7k.css} +1 -1
- package/dist/assets/{main-BFW0rGXU.js → main-Bwg5cpUh.js} +121 -121
- package/dist/assets/{web-M9cLwTrs.js → web-AyXD75n2.js} +1 -1
- package/dist/assets/{web-8gl2noUE.js → web-D9FjjUJx.js} +1 -1
- package/dist/assets/{web-D0uiHhnA.js → web-zhILDfYB.js} +1 -1
- package/dist/build-info.json +1 -1
- package/dist/index.html +2 -2
- package/dist/locales/en/terminal.json +2 -0
- package/dist/locales/es/terminal.json +1 -0
- package/dist/src/packages/server/claude/session-loader.js +98 -0
- package/dist/src/packages/server/codex/json-event-parser.js +99 -0
- package/dist/src/packages/server/index.js +4 -0
- package/dist/src/packages/server/services/claude-credentials-service.js +172 -13
- package/dist/src/packages/server/services/claude-usage-service.js +5 -1
- package/package.json +1 -1
- package/dist/assets/index-DIOhr3im.js +0 -19
- package/dist/assets/index-DqM0QltS.js +0 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
import{
|
|
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{
|
|
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{
|
|
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};
|
package/dist/build-info.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":"1.
|
|
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-
|
|
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-
|
|
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 {
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
339
|
+
sevenDayFable,
|
|
340
|
+
sevenDaySonnet: sevenDayFable,
|
|
337
341
|
},
|
|
338
342
|
error: null,
|
|
339
343
|
status: response.status,
|