tide-commander 1.177.2 → 1.178.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 +17 -0
- package/dist/assets/{BossLogsModal-BJ85hhZB.js → BossLogsModal-CS4LIvSu.js} +1 -1
- package/dist/assets/{BossSpawnModal-iF7jBjoW.js → BossSpawnModal-DFqNOCaI.js} +1 -1
- package/dist/assets/{ControlsModal-C-Dlj2C2.js → ControlsModal-BTX5H6jw.js} +1 -1
- package/dist/assets/{DockerLogsModal-CPK49emd.js → DockerLogsModal-C8pdXuqO.js} +1 -1
- package/dist/assets/{EmbeddedEditor-CtM591W2.js → EmbeddedEditor--FuYpTIa.js} +1 -1
- package/dist/assets/FcStdViewer-DwgwMwrx.js +1 -0
- package/dist/assets/GcodeViewer-CynUwDmS.js +1 -0
- package/dist/assets/{GmailOAuthSetup-DBbZpNj4.js → GmailOAuthSetup-BwdW6138.js} +1 -1
- package/dist/assets/{GoogleOAuthSetup-BF233CPj.js → GoogleOAuthSetup-D1r3EeDF.js} +1 -1
- package/dist/assets/{IframeModal-DoLqp8i3.js → IframeModal-B9yOGPoP.js} +1 -1
- package/dist/assets/{IntegrationsPanel-Dpx2slxW.js → IntegrationsPanel-lF8DIGck.js} +2 -2
- package/dist/assets/{LogViewerModal-CI6NVsnn.js → LogViewerModal-QtEg0GoV.js} +1 -1
- package/dist/assets/{MonitoringModal-CGQjCKh8.js → MonitoringModal-zHaxFAy8.js} +1 -1
- package/dist/assets/OrbitControls-B1__nj_c.js +1 -0
- package/dist/assets/{PM2LogsModal-cprJemBk.js → PM2LogsModal-DPRGsqvu.js} +1 -1
- package/dist/assets/{RestoreArchivedAreaModal-CD-nkwHT.js → RestoreArchivedAreaModal-4Yp2K-Vg.js} +1 -1
- package/dist/assets/{Scene2DCanvas-C0d2NOZF.js → Scene2DCanvas-Cn672tbC.js} +1 -1
- package/dist/assets/SceneManager-CKaBryd4.js +104 -0
- package/dist/assets/{SkillsPanel-Bea0jqNk.js → SkillsPanel-BNSoS60S.js} +1 -1
- package/dist/assets/{SlackMultiInstanceSetup-BvYk2UWs.js → SlackMultiInstanceSetup-BsckrPs-.js} +1 -1
- package/dist/assets/{SpawnModal-EOdsRMsU.js → SpawnModal-BxdMXDMp.js} +1 -1
- package/dist/assets/{StatisticsModal-DMTuwOyD.js → StatisticsModal-FNbvlYGU.js} +1 -1
- package/dist/assets/StlViewer-CRt0aUAf.js +1 -0
- package/dist/assets/{SubordinateAssignmentModal-kaXTALIo.js → SubordinateAssignmentModal-DxUqNMOR.js} +1 -1
- package/dist/assets/ThreeAreaSelector-DkbNFeb4.js +2 -0
- package/dist/assets/ThreeViewShortcuts-C6XYQvVm.js +1 -0
- package/dist/assets/{TriggerManagerPanel-BwmFtZCh.js → TriggerManagerPanel-DcX4J5m7.js} +1 -1
- package/dist/assets/{WorkflowEditorPanel-2SLzQ_F7.js → WorkflowEditorPanel-5VPqFGzD.js} +1 -1
- package/dist/assets/{abnfDiagram-VRR7QNED-Bn16ZJvn.js → abnfDiagram-VRR7QNED-6F-3T6-Y.js} +1 -1
- package/dist/assets/{arc-BS1yGfg8.js → arc-NHzv0EA5.js} +1 -1
- package/dist/assets/{architectureDiagram-ZJ3FMSHR-BowAVE2g.js → architectureDiagram-ZJ3FMSHR-CsJhNPjv.js} +1 -1
- package/dist/assets/{area-logos-iPYyTqRS.js → area-logos-CYEXA4lp.js} +1 -1
- package/dist/assets/{blockDiagram-677ZJIJ3-ZcRTeACC.js → blockDiagram-677ZJIJ3-UT3b2Q12.js} +1 -1
- package/dist/assets/{c4Diagram-LMCZKHZV-DVyCmWLe.js → c4Diagram-LMCZKHZV-uMYRTZix.js} +1 -1
- package/dist/assets/channel-D4Oo3ZHk.js +1 -0
- package/dist/assets/{chunk-2Q5K7J3B-Do4loXOf.js → chunk-2Q5K7J3B-BwXKcNaP.js} +1 -1
- package/dist/assets/{chunk-32BRIVSS-BteUBJrm.js → chunk-32BRIVSS-C8dNk4Hn.js} +1 -1
- package/dist/assets/{chunk-5VM5RSS4-CCzvXqtX.js → chunk-5VM5RSS4-Z2pZcBbh.js} +1 -1
- package/dist/assets/{chunk-EX3LRPZG-CWfoK8TS.js → chunk-EX3LRPZG-dA9bU_uS.js} +1 -1
- package/dist/assets/{chunk-JWPE2WC7-T2EVEO7f.js → chunk-JWPE2WC7-CMkfDWfd.js} +1 -1
- package/dist/assets/{chunk-MOJQB5TN-Bms_j_vp.js → chunk-MOJQB5TN-CRQhypCf.js} +1 -1
- package/dist/assets/{chunk-RYQCIY6F-BHaTGeSM.js → chunk-RYQCIY6F-CPvbNKNZ.js} +1 -1
- package/dist/assets/{chunk-V7JOEXUC-CTk13LTh.js → chunk-V7JOEXUC-6JsV0G3U.js} +1 -1
- package/dist/assets/{chunk-VR4S4FIN-DbAzJVcf.js → chunk-VR4S4FIN-n0aWG4-j.js} +1 -1
- package/dist/assets/{chunk-XXDRQBXY--nHyvZQ6.js → chunk-XXDRQBXY-BLQ81Hhu.js} +1 -1
- package/dist/assets/classDiagram-OUVF2IWQ-ChKBeWCX.js +1 -0
- package/dist/assets/classDiagram-v2-EOCWNBFH-ChKBeWCX.js +1 -0
- package/dist/assets/{cose-bilkent-JH36ORCC-D4STnGjf.js → cose-bilkent-JH36ORCC-BSHuRRPa.js} +1 -1
- package/dist/assets/{cynefin-VYW2F7L2-C3bGlbuH.js → cynefin-VYW2F7L2-vKDum-r-.js} +1 -1
- package/dist/assets/{cynefinDiagram-TSTJHNR4-NtQ7fqyK.js → cynefinDiagram-TSTJHNR4-B7hlff7z.js} +1 -1
- package/dist/assets/{dagre-VKFMJZFB-4_7veLhL.js → dagre-VKFMJZFB-K2lW3A2u.js} +1 -1
- package/dist/assets/{diagram-FQU43EPY-BXSSs1Tt.js → diagram-FQU43EPY-gtpv6DY0.js} +1 -1
- package/dist/assets/{diagram-G47NLZAW-0IkpFOVq.js → diagram-G47NLZAW-D9yHOqQq.js} +1 -1
- package/dist/assets/{diagram-NH7WQ7WH-DdjwsYrQ.js → diagram-NH7WQ7WH-BzeJ_2nM.js} +1 -1
- package/dist/assets/{diagram-OA4YK3LP-DVlmCA6A.js → diagram-OA4YK3LP-CM0HWugE.js} +1 -1
- package/dist/assets/{diagram-WEI45ONY-COn2Rq3e.js → diagram-WEI45ONY-BMyBFCkS.js} +1 -1
- package/dist/assets/{ebnfDiagram-CCIWWBDH-BBrme_H-.js → ebnfDiagram-CCIWWBDH-8Kj0FCEb.js} +1 -1
- package/dist/assets/{erDiagram-Q63AITRT-Dued2fRQ.js → erDiagram-Q63AITRT-BWDV58w3.js} +1 -1
- package/dist/assets/fcstd.worker-BSFnALBZ.js +24 -0
- package/dist/assets/{flowDiagram-23GEKE2U-CI3IU-MQ.js → flowDiagram-23GEKE2U-Bvu7iKuy.js} +1 -1
- package/dist/assets/{ganttDiagram-NO4QXBWP-DjLcWub4.js → ganttDiagram-NO4QXBWP-oOnRqEGi.js} +1 -1
- package/dist/assets/gcode.worker-DayjyJb4.js +1 -0
- package/dist/assets/{gitGraphDiagram-IHSO6WYX-CDGs8fL2.js → gitGraphDiagram-IHSO6WYX-BLNHz3A1.js} +1 -1
- package/dist/assets/{index-Ds6C-5SZ.js → index-BALtikL3.js} +1 -1
- package/dist/assets/{index-e1--CClk.js → index-BYRIJxh3.js} +3 -3
- package/dist/assets/{index-BeFfmS97.js → index-BZge8Vqz.js} +1 -1
- package/dist/assets/{index-BcJI4Wf2.js → index-Bzpy8ZrM.js} +1 -1
- package/dist/assets/index-CGjogkgR.js +2 -0
- package/dist/assets/{index-CPT2yDF7.js → index-CQrwfFmj.js} +3 -3
- package/dist/assets/{index-BO9rcCxW.js → index-DaieR5TQ.js} +1 -1
- package/dist/assets/{index-DY0EdedI.js → index-DcPCPN6v.js} +6 -6
- package/dist/assets/index-DhnKpoWm.js +2 -0
- package/dist/assets/{infoDiagram-FWYZ7A6U-BR8lw-07.js → infoDiagram-FWYZ7A6U-CBoekTgL.js} +1 -1
- package/dist/assets/{ishikawaDiagram-FXEZZL3T-BvoUdvxM.js → ishikawaDiagram-FXEZZL3T-i92jYFyn.js} +1 -1
- package/dist/assets/{journeyDiagram-5HDEW3XC-5ZTKSL91.js → journeyDiagram-5HDEW3XC-Dlb8tdvI.js} +1 -1
- package/dist/assets/{kanban-definition-HUTT4EX6-Crw59w7d.js → kanban-definition-HUTT4EX6-C357GaQI.js} +1 -1
- package/dist/assets/{linear-BClkkbnW.js → linear-CIJgBkf3.js} +1 -1
- package/dist/assets/{main-anrHq8ki.js → main-DN8Eh8GZ.js} +144 -144
- package/dist/assets/main-ZtOMcfnf.css +1 -0
- package/dist/assets/{mermaid.core-BDhiHGyU.js → mermaid.core-DpoI2o0n.js} +4 -4
- package/dist/assets/{mindmap-definition-LN4V7U3C-kGtzuV8p.js → mindmap-definition-LN4V7U3C-C5jHYqmi.js} +1 -1
- package/dist/assets/occt-import-js-BhHfLpto.wasm +0 -0
- package/dist/assets/{pegDiagram-2B236MQR-DacPFZVr.js → pegDiagram-2B236MQR-CxZiEIc6.js} +1 -1
- package/dist/assets/{pieDiagram-ENE6RG2P-a7t3bV4m.js → pieDiagram-ENE6RG2P-DP--XMmv.js} +1 -1
- package/dist/assets/{quadrantDiagram-ABIIQ3AL-D1LJoytH.js → quadrantDiagram-ABIIQ3AL-SVVI0nj0.js} +1 -1
- package/dist/assets/{railroadDiagram-RFXS5EU6-DtgETCCu.js → railroadDiagram-RFXS5EU6-B96CUfFG.js} +1 -1
- package/dist/assets/{requirementDiagram-TGXJPOKE-B8ofroLn.js → requirementDiagram-TGXJPOKE-CY0Y9Spd.js} +1 -1
- package/dist/assets/{sankeyDiagram-HTMAVEWB-BLJxfQiX.js → sankeyDiagram-HTMAVEWB-DUwF25eW.js} +1 -1
- package/dist/assets/{sequenceDiagram-DBY2YBRQ-DkXTwgge.js → sequenceDiagram-DBY2YBRQ-B1BXiSWm.js} +1 -1
- package/dist/assets/{sizeCapture-X5ZJPWSS-Tu0vJw5U.js → sizeCapture-X5ZJPWSS-DOInCz7x.js} +1 -1
- package/dist/assets/{stateDiagram-2N3HPSRC-Y8r1KoB6.js → stateDiagram-2N3HPSRC-DbJfBNBw.js} +1 -1
- package/dist/assets/stateDiagram-v2-6OUMAXLB-Cw6k7KjM.js +1 -0
- package/dist/assets/{swimlanes-5IMT3BWC-BFl--h7F.js → swimlanes-5IMT3BWC-BiSKR-2-.js} +2 -2
- package/dist/assets/swimlanesDiagram-G3AALYLV-De7ovgag.js +8 -0
- package/dist/assets/{timeline-definition-FHXFAJF6-CVxVk8L_.js → timeline-definition-FHXFAJF6-CjofGPLC.js} +1 -1
- package/dist/assets/{vendor-three-3OKhI9bz.js → vendor-three-CpheMTQa.js} +169 -169
- package/dist/assets/{vennDiagram-L72KCM5P-BfIiqSop.js → vennDiagram-L72KCM5P-B_6UlK4t.js} +1 -1
- package/dist/assets/{wardleyDiagram-EHGQE667-K-vB1d_8.js → wardleyDiagram-EHGQE667-CdJsoBXi.js} +1 -1
- package/dist/assets/{web-BWzU4tbP.js → web-DEGVeF3E.js} +1 -1
- package/dist/assets/{web-sV6-seS-.js → web-DINOAJn_.js} +1 -1
- package/dist/assets/{web-e-nG7wcx.js → web-Dt2qR7v-.js} +1 -1
- package/dist/assets/{xychartDiagram-FW5EYKEG-D6q7J6Kr.js → xychartDiagram-FW5EYKEG-Cp2dbS-J.js} +1 -1
- package/dist/build-info.json +1 -1
- package/dist/index.html +3 -3
- package/dist/locales/en/terminal.json +90 -1
- package/dist/locales/es/terminal.json +92 -2
- package/dist/src/packages/server/codex/app-server/app-server-process.js +153 -12
- package/dist/src/packages/server/routes/files.js +80 -6
- package/dist/src/packages/server/routes/system.js +9 -2
- package/package.json +2 -1
- package/dist/assets/SceneManager-CtgRFnMf.js +0 -104
- package/dist/assets/channel-CohCoDiY.js +0 -1
- package/dist/assets/classDiagram-OUVF2IWQ-BxHRM6o6.js +0 -1
- package/dist/assets/classDiagram-v2-EOCWNBFH-BxHRM6o6.js +0 -1
- package/dist/assets/index-CD2Gv3oe.js +0 -2
- package/dist/assets/index-evqtZdbD.js +0 -2
- package/dist/assets/main-BNsRZCIt.css +0 -1
- package/dist/assets/stateDiagram-v2-6OUMAXLB-2qL1wJnY.js +0 -1
- package/dist/assets/swimlanesDiagram-G3AALYLV-CrYKOH_2.js +0 -8
|
@@ -20,6 +20,12 @@
|
|
|
20
20
|
*
|
|
21
21
|
* One process multiplexes every Codex agent: each agent owns a `threadId`, and
|
|
22
22
|
* notifications carry that id so the runner can fan them back out.
|
|
23
|
+
*
|
|
24
|
+
* The daemon reads ~/.codex/auth.json ONCE at startup and never re-reads it, so
|
|
25
|
+
* a daemon that outlives an account switch keeps running every agent on the
|
|
26
|
+
* PREVIOUS account. Both entry points guard against that: boot() refuses to
|
|
27
|
+
* rejoin a daemon whose account no longer matches the auth file, and
|
|
28
|
+
* `killDetachedAppServerDaemon()` is called by the account switcher.
|
|
23
29
|
*/
|
|
24
30
|
import { spawn } from 'child_process';
|
|
25
31
|
import * as http from 'http';
|
|
@@ -27,11 +33,14 @@ import * as net from 'net';
|
|
|
27
33
|
import * as fs from 'fs';
|
|
28
34
|
import { WebSocket } from 'ws';
|
|
29
35
|
import { createLogger } from '../../utils/logger.js';
|
|
36
|
+
import { listProviderCredentialProfiles } from '../../services/provider-credentials-service.js';
|
|
30
37
|
import { loadDaemonInfo, saveDaemonInfo, clearDaemonInfo, APP_SERVER_LOG_FILE, } from './app-server-recovery-store.js';
|
|
31
38
|
const log = createLogger('CodexAppServer');
|
|
32
39
|
const INITIALIZE_TIMEOUT_MS = 15000;
|
|
33
40
|
const READYZ_TIMEOUT_MS = 20000;
|
|
34
41
|
const READYZ_POLL_MS = 250;
|
|
42
|
+
const ACCOUNT_PROBE_TIMEOUT_MS = 5000;
|
|
43
|
+
const DAEMON_KILL_GRACE_MS = 2000;
|
|
35
44
|
export class CodexAppServerProcess {
|
|
36
45
|
opts;
|
|
37
46
|
ws = null;
|
|
@@ -60,14 +69,21 @@ export class CodexAppServerProcess {
|
|
|
60
69
|
}
|
|
61
70
|
async boot() {
|
|
62
71
|
this.stopped = false;
|
|
63
|
-
// 1. Rejoin an already-running daemon if one is healthy
|
|
72
|
+
// 1. Rejoin an already-running daemon if one is healthy AND still logged in
|
|
73
|
+
// as the account ~/.codex/auth.json currently holds. A daemon that
|
|
74
|
+
// survived an account switch (ours or a plain `codex login`) would keep
|
|
75
|
+
// every agent on the old, usually rate-limited, account.
|
|
64
76
|
const existing = loadDaemonInfo();
|
|
65
77
|
if (existing && (await this.isDaemonHealthy(existing))) {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
78
|
+
if (await daemonAccountIsCurrent(existing.port)) {
|
|
79
|
+
log.log(`🔗 Rejoining existing app-server daemon pid=${existing.pid} port=${existing.port}`);
|
|
80
|
+
this.port = existing.port;
|
|
81
|
+
this.reconnectedToExisting = true;
|
|
82
|
+
await this.connectAndInitialize(existing.port);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
log.warn(`Existing app-server daemon pid=${existing.pid} is signed in as a different Codex account than ~/.codex/auth.json — killing it so the active account takes effect`);
|
|
86
|
+
killDetachedAppServerDaemon();
|
|
71
87
|
}
|
|
72
88
|
// 2. Spawn a fresh detached daemon.
|
|
73
89
|
this.reconnectedToExisting = false;
|
|
@@ -253,15 +269,140 @@ export class CodexAppServerProcess {
|
|
|
253
269
|
/** Disconnect AND kill the daemon (used on explicit full shutdown). */
|
|
254
270
|
killDaemon() {
|
|
255
271
|
this.disconnect();
|
|
256
|
-
|
|
257
|
-
|
|
272
|
+
killDetachedAppServerDaemon();
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Kill the detached daemon recorded on disk, if any, and forget it.
|
|
277
|
+
*
|
|
278
|
+
* Called when the live Codex account changes: the daemon caches
|
|
279
|
+
* ~/.codex/auth.json in memory at startup, so switching accounts is a silent
|
|
280
|
+
* no-op for every agent until the daemon is replaced. The next launch spawns a
|
|
281
|
+
* fresh one with the new credentials; in-flight turns die with the old daemon
|
|
282
|
+
* (they were running on the account the user just abandoned), and the runner's
|
|
283
|
+
* socket-close handler finalizes those agents so none stays stuck 'working'.
|
|
284
|
+
*/
|
|
285
|
+
export function killDetachedAppServerDaemon() {
|
|
286
|
+
const info = loadDaemonInfo();
|
|
287
|
+
if (!info)
|
|
288
|
+
return { stopped: false, pid: null };
|
|
289
|
+
const alive = isPidAlive(info.pid);
|
|
290
|
+
if (alive) {
|
|
291
|
+
// The recorded pid is the `codex` node wrapper, which spawns the real Rust
|
|
292
|
+
// binary as a child — signal the whole process GROUP (spawn used
|
|
293
|
+
// detached:true, so the wrapper leads one) or the binary outlives the
|
|
294
|
+
// wrapper and keeps holding the old account. SIGKILL the stragglers after a
|
|
295
|
+
// grace period; the timer is unref'd so it can't hold the process open.
|
|
296
|
+
signalDaemon(info.pid, 'SIGTERM');
|
|
297
|
+
log.log(`🔪 Killed app-server daemon pid=${info.pid} (Codex credentials changed)`);
|
|
298
|
+
setTimeout(() => {
|
|
299
|
+
if (isPidAlive(info.pid))
|
|
300
|
+
signalDaemon(info.pid, 'SIGKILL');
|
|
301
|
+
}, DAEMON_KILL_GRACE_MS).unref();
|
|
302
|
+
}
|
|
303
|
+
clearDaemonInfo();
|
|
304
|
+
return { stopped: alive, pid: alive ? info.pid : null };
|
|
305
|
+
}
|
|
306
|
+
function signalDaemon(pid, signal) {
|
|
307
|
+
try {
|
|
308
|
+
process.kill(-pid, signal);
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
catch { /* not a group leader — fall back to the pid itself */ }
|
|
312
|
+
try {
|
|
313
|
+
process.kill(pid, signal);
|
|
314
|
+
}
|
|
315
|
+
catch (err) {
|
|
316
|
+
log.warn(`Failed to ${signal} app-server daemon pid=${pid}: ${err.message}`);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Compare the account a daemon is signed in as against the one in the auth
|
|
321
|
+
* file. Anything unknown (API-key auth, unreachable daemon, probe error) counts
|
|
322
|
+
* as current — a probe glitch must never cost the user a running daemon.
|
|
323
|
+
*/
|
|
324
|
+
export function isDaemonAccountCurrent(daemonEmail, activeEmail) {
|
|
325
|
+
if (!daemonEmail || !activeEmail)
|
|
326
|
+
return true;
|
|
327
|
+
return daemonEmail.trim().toLowerCase() === activeEmail.trim().toLowerCase();
|
|
328
|
+
}
|
|
329
|
+
async function daemonAccountIsCurrent(port) {
|
|
330
|
+
let activeEmail = null;
|
|
331
|
+
try {
|
|
332
|
+
activeEmail = listProviderCredentialProfiles('codex').active?.email ?? null;
|
|
333
|
+
}
|
|
334
|
+
catch {
|
|
335
|
+
return true;
|
|
336
|
+
}
|
|
337
|
+
if (!activeEmail)
|
|
338
|
+
return true;
|
|
339
|
+
return isDaemonAccountCurrent(await probeDaemonAccountEmail(port), activeEmail);
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* Ask a running daemon which ChatGPT account it holds, over a throwaway
|
|
343
|
+
* WebSocket so the runner's own connection/lifecycle is untouched. Returns null
|
|
344
|
+
* when the answer isn't a ChatGPT login (API key) or the probe fails.
|
|
345
|
+
*/
|
|
346
|
+
function probeDaemonAccountEmail(port) {
|
|
347
|
+
return new Promise((resolve) => {
|
|
348
|
+
let ws;
|
|
349
|
+
try {
|
|
350
|
+
ws = new WebSocket(`ws://127.0.0.1:${port}`);
|
|
351
|
+
}
|
|
352
|
+
catch {
|
|
353
|
+
resolve(null);
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
let settled = false;
|
|
357
|
+
const finish = (email) => {
|
|
358
|
+
if (settled)
|
|
359
|
+
return;
|
|
360
|
+
settled = true;
|
|
361
|
+
clearTimeout(timer);
|
|
258
362
|
try {
|
|
259
|
-
|
|
363
|
+
ws.close();
|
|
260
364
|
}
|
|
261
365
|
catch { /* ignore */ }
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
366
|
+
resolve(email);
|
|
367
|
+
};
|
|
368
|
+
const timer = setTimeout(() => finish(null), ACCOUNT_PROBE_TIMEOUT_MS);
|
|
369
|
+
ws.on('open', () => {
|
|
370
|
+
ws.send(JSON.stringify({
|
|
371
|
+
id: 1,
|
|
372
|
+
method: 'initialize',
|
|
373
|
+
params: {
|
|
374
|
+
clientInfo: { name: 'tide-commander', title: 'Tide Commander', version: '1.0.0' },
|
|
375
|
+
capabilities: { experimentalApi: true },
|
|
376
|
+
},
|
|
377
|
+
}));
|
|
378
|
+
});
|
|
379
|
+
ws.on('message', (data) => {
|
|
380
|
+
let msg;
|
|
381
|
+
try {
|
|
382
|
+
msg = JSON.parse(data.toString());
|
|
383
|
+
}
|
|
384
|
+
catch {
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
if (msg.id === undefined || msg.method !== undefined)
|
|
388
|
+
return;
|
|
389
|
+
if (Number(msg.id) === 1) {
|
|
390
|
+
if (msg.error) {
|
|
391
|
+
finish(null);
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
ws.send(JSON.stringify({ method: 'initialized', params: {} }));
|
|
395
|
+
ws.send(JSON.stringify({ id: 2, method: 'account/read', params: {} }));
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
if (Number(msg.id) === 2) {
|
|
399
|
+
const account = msg.result?.account;
|
|
400
|
+
finish(typeof account?.email === 'string' ? account.email : null);
|
|
401
|
+
}
|
|
402
|
+
});
|
|
403
|
+
ws.on('error', () => finish(null));
|
|
404
|
+
ws.on('close', () => finish(null));
|
|
405
|
+
});
|
|
265
406
|
}
|
|
266
407
|
function isPidAlive(pid) {
|
|
267
408
|
if (!pid || pid < 1)
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import { Router } from 'express';
|
|
6
6
|
import * as fs from 'fs';
|
|
7
7
|
import * as path from 'path';
|
|
8
|
-
import { execFile, execSync, spawn } from 'child_process';
|
|
8
|
+
import { execFile, execFileSync, execSync, spawn } from 'child_process';
|
|
9
9
|
import * as os from 'os';
|
|
10
10
|
import { pathToFileURL } from 'url';
|
|
11
11
|
import { logger } from '../utils/logger.js';
|
|
@@ -21,6 +21,23 @@ if (!fs.existsSync(TEMP_DIR)) {
|
|
|
21
21
|
}
|
|
22
22
|
log.log(` Temp upload directory: ${TEMP_DIR}`);
|
|
23
23
|
const router = Router();
|
|
24
|
+
function looksLikeBinaryBuffer(buffer) {
|
|
25
|
+
if (buffer.length === 0)
|
|
26
|
+
return false;
|
|
27
|
+
const sample = buffer.subarray(0, Math.min(buffer.length, 8192));
|
|
28
|
+
// NUL catches the common case; replacement characters catch compressed or
|
|
29
|
+
// media formats that contain no NUL in their first block but are not UTF-8.
|
|
30
|
+
return sample.includes(0) || buffer.toString('utf-8').includes('\uFFFD');
|
|
31
|
+
}
|
|
32
|
+
function binaryContentTypeForExtension(extension) {
|
|
33
|
+
const mimeTypes = {
|
|
34
|
+
'.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg',
|
|
35
|
+
'.gif': 'image/gif', '.webp': 'image/webp', '.bmp': 'image/bmp',
|
|
36
|
+
'.ico': 'image/x-icon', '.pdf': 'application/pdf', '.stl': 'model/stl',
|
|
37
|
+
'.fcstd': 'application/vnd.freecad', '.gcode': 'text/x-gcode', '.gco': 'text/x-gcode',
|
|
38
|
+
};
|
|
39
|
+
return mimeTypes[extension] || 'application/octet-stream';
|
|
40
|
+
}
|
|
24
41
|
// Windows absolute paths — drive-letter (`C:\…` or `C:/…`) and UNC (`\\server\share`)
|
|
25
42
|
// — are NOT recognized by path.isAbsolute() when the server runs on POSIX. Without
|
|
26
43
|
// this, a configured Windows path is misclassified as relative and resolved into
|
|
@@ -546,7 +563,7 @@ router.get('/read', async (req, res) => {
|
|
|
546
563
|
res.status(400).json({ error: 'File too large (max 1MB)' });
|
|
547
564
|
return;
|
|
548
565
|
}
|
|
549
|
-
const
|
|
566
|
+
const fileBuffer = fs.readFileSync(filePath);
|
|
550
567
|
const extension = path.extname(filePath).toLowerCase();
|
|
551
568
|
const filename = path.basename(filePath);
|
|
552
569
|
const base = {
|
|
@@ -559,6 +576,11 @@ router.get('/read', async (req, res) => {
|
|
|
559
576
|
areaId: resolution.areaId,
|
|
560
577
|
areaName: resolution.areaName,
|
|
561
578
|
};
|
|
579
|
+
if (looksLikeBinaryBuffer(fileBuffer)) {
|
|
580
|
+
res.status(415).json({ ...base, binary: true, error: 'Binary file cannot be read as text' });
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
const content = fileBuffer.toString('utf-8');
|
|
562
584
|
// Preview mode (Ctrl+hover tooltip in the terminal): return only the
|
|
563
585
|
// requested line window. Reading the file is cheap; shipping a 1MB body
|
|
564
586
|
// over the wire on every hover is not.
|
|
@@ -762,6 +784,10 @@ router.get('/binary', async (req, res) => {
|
|
|
762
784
|
'.ico': 'image/x-icon',
|
|
763
785
|
'.svg': 'image/svg+xml',
|
|
764
786
|
'.pdf': 'application/pdf',
|
|
787
|
+
'.stl': 'model/stl',
|
|
788
|
+
'.fcstd': 'application/vnd.freecad',
|
|
789
|
+
'.gcode': 'text/x-gcode',
|
|
790
|
+
'.gco': 'text/x-gcode',
|
|
765
791
|
'.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
766
792
|
'.xls': 'application/vnd.ms-excel',
|
|
767
793
|
'.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
@@ -1817,11 +1843,10 @@ router.get('/git-original', async (req, res) => {
|
|
|
1817
1843
|
// Get relative path from git root
|
|
1818
1844
|
const relativePath = path.relative(gitRoot, filePath);
|
|
1819
1845
|
// Get original content from HEAD
|
|
1820
|
-
let
|
|
1846
|
+
let originalBuffer;
|
|
1821
1847
|
try {
|
|
1822
|
-
|
|
1848
|
+
originalBuffer = execFileSync('git', ['show', `HEAD:${relativePath}`], {
|
|
1823
1849
|
cwd: gitRoot,
|
|
1824
|
-
encoding: 'utf-8',
|
|
1825
1850
|
maxBuffer: 10 * 1024 * 1024, // 10MB
|
|
1826
1851
|
});
|
|
1827
1852
|
}
|
|
@@ -1839,11 +1864,22 @@ router.get('/git-original', async (req, res) => {
|
|
|
1839
1864
|
}
|
|
1840
1865
|
throw err;
|
|
1841
1866
|
}
|
|
1867
|
+
if (looksLikeBinaryBuffer(originalBuffer)) {
|
|
1868
|
+
res.status(415).json({
|
|
1869
|
+
path: filePath,
|
|
1870
|
+
filename: path.basename(filePath),
|
|
1871
|
+
extension: path.extname(filePath).toLowerCase(),
|
|
1872
|
+
binary: true,
|
|
1873
|
+
isNew: false,
|
|
1874
|
+
error: 'Binary file cannot be read as text',
|
|
1875
|
+
});
|
|
1876
|
+
return;
|
|
1877
|
+
}
|
|
1842
1878
|
res.json({
|
|
1843
1879
|
path: filePath,
|
|
1844
1880
|
filename: path.basename(filePath),
|
|
1845
1881
|
extension: path.extname(filePath).toLowerCase(),
|
|
1846
|
-
content:
|
|
1882
|
+
content: originalBuffer.toString('utf-8'),
|
|
1847
1883
|
isNew: false,
|
|
1848
1884
|
});
|
|
1849
1885
|
}
|
|
@@ -1852,6 +1888,40 @@ router.get('/git-original', async (req, res) => {
|
|
|
1852
1888
|
res.status(500).json({ error: err.message });
|
|
1853
1889
|
}
|
|
1854
1890
|
});
|
|
1891
|
+
// GET /api/files/git-original-binary - Stream the HEAD version of a binary
|
|
1892
|
+
// file. This keeps deleted image/PDF/STL/FCStd previews useful in the Git modal.
|
|
1893
|
+
router.get('/git-original-binary', async (req, res) => {
|
|
1894
|
+
try {
|
|
1895
|
+
const resolution = resolveAndValidateFilePath(req.query.path, req.query.baseDir);
|
|
1896
|
+
if (!resolution.ok) {
|
|
1897
|
+
res.status(resolution.status).json({ error: resolution.error });
|
|
1898
|
+
return;
|
|
1899
|
+
}
|
|
1900
|
+
const filePath = resolution.path;
|
|
1901
|
+
const gitRoot = execFileSync('git', ['rev-parse', '--show-toplevel'], {
|
|
1902
|
+
cwd: path.dirname(filePath),
|
|
1903
|
+
encoding: 'utf-8',
|
|
1904
|
+
}).trim();
|
|
1905
|
+
const relativePath = path.relative(gitRoot, filePath);
|
|
1906
|
+
if (!relativePath || relativePath.startsWith('..') || path.isAbsolute(relativePath)) {
|
|
1907
|
+
res.status(400).json({ error: 'Path is outside the git repository' });
|
|
1908
|
+
return;
|
|
1909
|
+
}
|
|
1910
|
+
const buffer = execFileSync('git', ['show', `HEAD:${relativePath}`], {
|
|
1911
|
+
cwd: gitRoot,
|
|
1912
|
+
maxBuffer: 50 * 1024 * 1024,
|
|
1913
|
+
});
|
|
1914
|
+
const extension = path.extname(filePath).toLowerCase();
|
|
1915
|
+
res.setHeader('Content-Type', binaryContentTypeForExtension(extension));
|
|
1916
|
+
res.setHeader('Content-Length', buffer.length);
|
|
1917
|
+
res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate');
|
|
1918
|
+
res.send(buffer);
|
|
1919
|
+
}
|
|
1920
|
+
catch (err) {
|
|
1921
|
+
log.error(' Failed to get original binary from git:', err);
|
|
1922
|
+
res.status(404).json({ error: 'Original binary file not found' });
|
|
1923
|
+
}
|
|
1924
|
+
});
|
|
1855
1925
|
// GET /api/files/git-diff - Get unified diff for a file
|
|
1856
1926
|
router.get('/git-diff', async (req, res) => {
|
|
1857
1927
|
try {
|
|
@@ -3093,6 +3163,10 @@ router.post('/by-path', async (req, res) => {
|
|
|
3093
3163
|
'.mp3': 'audio/mpeg',
|
|
3094
3164
|
'.mp4': 'video/mp4',
|
|
3095
3165
|
'.wav': 'audio/wav',
|
|
3166
|
+
'.stl': 'model/stl',
|
|
3167
|
+
'.fcstd': 'application/vnd.freecad',
|
|
3168
|
+
'.gcode': 'text/x-gcode',
|
|
3169
|
+
'.gco': 'text/x-gcode',
|
|
3096
3170
|
};
|
|
3097
3171
|
const contentType = mimeTypes[extension] || 'application/octet-stream';
|
|
3098
3172
|
res.setHeader('Content-Type', contentType);
|
|
@@ -15,6 +15,7 @@ import { isDevInstall, maybeTriggerWebBundleRebuild } from '../services/web-bund
|
|
|
15
15
|
import { deleteClaudeCredentialProfile, getClaudeCredentialProfilesUsage, listClaudeCredentialProfiles, renameClaudeCredentialProfile, saveActiveClaudeCredentialProfile, switchClaudeCredentialProfile, } from '../services/claude-credentials-service.js';
|
|
16
16
|
import { deleteProviderCredentialProfile, listProviderCredentialProfiles, renameProviderCredentialProfile, saveActiveProviderCredentialProfile, switchProviderCredentialProfile, } from '../services/provider-credentials-service.js';
|
|
17
17
|
import { getCodexCredentialProfilesUsage } from '../services/codex-usage-service.js';
|
|
18
|
+
import { killDetachedAppServerDaemon } from '../codex/app-server/app-server-process.js';
|
|
18
19
|
const log = createLogger('SystemRoutes');
|
|
19
20
|
const router = Router();
|
|
20
21
|
const PACKAGE_NAME = 'tide-commander';
|
|
@@ -609,9 +610,15 @@ function mountProviderCredentialRoutes(provider, basePath) {
|
|
|
609
610
|
res.status(400).json({ error: 'stashActiveAs must be a string when provided' });
|
|
610
611
|
return;
|
|
611
612
|
}
|
|
612
|
-
|
|
613
|
+
const result = switchProviderCredentialProfile(provider, name.trim(), {
|
|
613
614
|
stashActiveAs: typeof stashActiveAs === 'string' ? stashActiveAs.trim() : undefined,
|
|
614
|
-
})
|
|
615
|
+
});
|
|
616
|
+
// The detached `codex app-server` daemon caches ~/.codex/auth.json in
|
|
617
|
+
// memory at startup, so without this every Codex agent would keep running
|
|
618
|
+
// on the account the user just switched away from. Killing it makes the
|
|
619
|
+
// next launch spawn a fresh daemon with the new credentials.
|
|
620
|
+
const appServerStopped = provider === 'codex' ? killDetachedAppServerDaemon().stopped : false;
|
|
621
|
+
res.json({ ...result, codexAppServerStopped: appServerStopped });
|
|
615
622
|
}
|
|
616
623
|
catch (err) {
|
|
617
624
|
const message = err.message;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tide-commander",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.178.0",
|
|
4
4
|
"description": "Visual multi-agent orchestrator and manager for Claude Code with 3D/2D interface",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -91,6 +91,7 @@
|
|
|
91
91
|
"mssql": "^12.4.0",
|
|
92
92
|
"multer": "^2.1.1",
|
|
93
93
|
"mysql2": "^3.16.2",
|
|
94
|
+
"occt-import-js": "^0.0.23",
|
|
94
95
|
"oracledb": "^6.10.0",
|
|
95
96
|
"pdfjs-dist": "^5.6.205",
|
|
96
97
|
"pg": "^8.17.2",
|