tide-commander 1.78.0 → 1.80.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 (41) hide show
  1. package/dist/__seed_auth__.html +15 -0
  2. package/dist/assets/{BossLogsModal-BJUaVgOz.js → BossLogsModal-BLPz-zG4.js} +1 -1
  3. package/dist/assets/{BossSpawnModal-CLzUp9FQ.js → BossSpawnModal-Bq1v2vF7.js} +1 -1
  4. package/dist/assets/{ControlsModal-BK-nIx6P.js → ControlsModal-C89UyHcA.js} +1 -1
  5. package/dist/assets/{DockerLogsModal-wW7EV327.js → DockerLogsModal-j2ccIOAn.js} +1 -1
  6. package/dist/assets/{EmbeddedEditor-B7LC5gF5.js → EmbeddedEditor-DbPvjE6L.js} +1 -1
  7. package/dist/assets/{GmailOAuthSetup-C_kETU4w.js → GmailOAuthSetup-BpXMS91b.js} +1 -1
  8. package/dist/assets/{GoogleOAuthSetup-Dn1hZsMs.js → GoogleOAuthSetup-e7A0yZBj.js} +1 -1
  9. package/dist/assets/{IframeModal-8t96ASFF.js → IframeModal-BWSM0KU4.js} +1 -1
  10. package/dist/assets/{IntegrationsPanel-C0TjN-6q.js → IntegrationsPanel-C9Zpu_4p.js} +2 -2
  11. package/dist/assets/{LogViewerModal-J_Wv4RxY.js → LogViewerModal-D_S9YZRw.js} +1 -1
  12. package/dist/assets/{MonitoringModal-CUy6EbeR.js → MonitoringModal-ClOTO0Uf.js} +1 -1
  13. package/dist/assets/{PM2LogsModal-CgYX33GL.js → PM2LogsModal-BTtFTV-5.js} +1 -1
  14. package/dist/assets/{RestoreArchivedAreaModal-ILOa64df.js → RestoreArchivedAreaModal-_4ptM0i8.js} +1 -1
  15. package/dist/assets/{Scene2DCanvas-B9_c0Cr2.js → Scene2DCanvas-Bk8TmqUL.js} +1 -1
  16. package/dist/assets/{SceneManager-BuMbzzM8.js → SceneManager-DvTJnHJW.js} +1 -1
  17. package/dist/assets/{SkillsPanel-Bp0UiQRG.js → SkillsPanel-t1aTNgcy.js} +1 -1
  18. package/dist/assets/{SpawnModal-DjzhiAMO.js → SpawnModal-C0bzyux7.js} +1 -1
  19. package/dist/assets/{SubordinateAssignmentModal-CWaAMKp5.js → SubordinateAssignmentModal-CoZ8oWod.js} +1 -1
  20. package/dist/assets/{TriggerManagerPanel-DpCFDRa5.js → TriggerManagerPanel-DdqYdLrT.js} +1 -1
  21. package/dist/assets/{WorkflowEditorPanel-D-Vcfxmm.js → WorkflowEditorPanel-hoviKYSD.js} +1 -1
  22. package/dist/assets/{index-7TaAASV1.js → index-B9l1lyBZ.js} +2 -2
  23. package/dist/assets/{index-rGRqmeiB.js → index-CRvdO7Kd.js} +1 -1
  24. package/dist/assets/{index-DyidqysN.js → index-CmD0ROmj.js} +1 -1
  25. package/dist/assets/{index-BRLyVoUe.js → index-D2pHMTZ5.js} +1 -1
  26. package/dist/assets/{index-DX-7fZ78.js → index-DDyLKl-p.js} +3 -3
  27. package/dist/assets/{index-BeUHeagl.js → index-DEhCjM_2.js} +1 -1
  28. package/dist/assets/{index-D6--gGWa.js → index-DGoXs77Q.js} +1 -1
  29. package/dist/assets/{index-1yqGGDiA.js → index-DZtAf5eK.js} +1 -1
  30. package/dist/assets/{index-C2Fyjj0L.css → index-OGMIDcJ3.css} +1 -1
  31. package/dist/assets/{index-COln_-Fk.js → index-_voSk9UQ.js} +1 -1
  32. package/dist/assets/main-D1tIQI_9.css +1 -0
  33. package/dist/assets/{main-COvyQg-T.js → main-D8MIGzmr.js} +70 -70
  34. package/dist/assets/{web-N_FykYu_.js → web-BrE2NRKk.js} +1 -1
  35. package/dist/assets/{web-CpCf-DCG.js → web-cmqeYKAz.js} +1 -1
  36. package/dist/index.html +2 -2
  37. package/dist/src/packages/server/codex/backend.js +5 -0
  38. package/dist/src/packages/server/codex/json-event-parser.js +19 -4
  39. package/dist/src/packages/server/routes/files.js +67 -68
  40. package/package.json +1 -1
  41. package/dist/assets/main-Co2mNETL.css +0 -1
@@ -1 +1 @@
1
- import{c5 as a}from"./main-COvyQg-T.js";import{ImpactStyle as i,NotificationType as r}from"./index-7TaAASV1.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{c5 as a}from"./main-D8MIGzmr.js";import{ImpactStyle as i,NotificationType as r}from"./index-B9l1lyBZ.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{c5 as s}from"./main-COvyQg-T.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{c5 as s}from"./main-D8MIGzmr.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/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-COvyQg-T.js"></script>
25
+ <script type="module" crossorigin src="/assets/main-D8MIGzmr.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-Co2mNETL.css">
28
+ <link rel="stylesheet" crossorigin href="/assets/main-D1tIQI_9.css">
29
29
  </head>
30
30
  <body>
31
31
  <div id="app"></div>
@@ -63,6 +63,11 @@ export class CodexBackend {
63
63
  this.parser.setWorkingDirectory(config.workingDir);
64
64
  this.pendingStdinPrompt = buildCodexPrompt(config);
65
65
  const args = ['exec', '--experimental-json'];
66
+ // Codex renamed [features].collab → [features].multi_agent. Enable the new
67
+ // flag explicitly so subagent orchestration (collab_tool_call items) works
68
+ // without the user needing `[features].collab = true` in ~/.codex/config.toml,
69
+ // which Codex now emits a deprecation error for on every turn.
70
+ args.push('--enable', 'multi_agent');
66
71
  const codexConfig = config.codexConfig;
67
72
  const fullAuto = codexConfig?.fullAuto !== false;
68
73
  if (fullAuto) {
@@ -85,6 +85,8 @@ function parseItem(item) {
85
85
  receiver_thread_ids: asStringArray(item.receiver_thread_ids),
86
86
  prompt: item.prompt === null ? null : asString(item.prompt),
87
87
  agents_states: parseCollabAgentStates(item.agents_states),
88
+ message: asString(item.message),
89
+ error: asString(item.error),
88
90
  };
89
91
  }
90
92
  function parseUsage(usage) {
@@ -147,6 +149,7 @@ function parseResponsePayload(payload) {
147
149
  export class CodexJsonEventParser {
148
150
  activeToolByItemId = new Map();
149
151
  lastAgentMessageText;
152
+ lastErrorText;
150
153
  lastModelUsageSnapshot;
151
154
  enableFileDiffEnrichment;
152
155
  workingDirectory;
@@ -409,6 +412,11 @@ export class CodexJsonEventParser {
409
412
  parseItemCompleted(item) {
410
413
  if (!item?.type)
411
414
  return [];
415
+ if (item.type === 'error') {
416
+ const errorMessage = item.message || item.text || item.error || 'Codex emitted an error';
417
+ this.lastErrorText = errorMessage;
418
+ return [{ type: 'error', errorMessage }];
419
+ }
412
420
  if (item.type === 'reasoning' && item.text) {
413
421
  return [{ type: 'thinking', text: item.text, isStreaming: false }];
414
422
  }
@@ -536,10 +544,17 @@ export class CodexJsonEventParser {
536
544
  cacheRead: usage.cached_input_tokens,
537
545
  },
538
546
  };
539
- // Include the last agent message as resultText for boss delegation processing
540
- if (this.lastAgentMessageText) {
541
- event.resultText = this.lastAgentMessageText;
542
- this.lastAgentMessageText = undefined; // Reset for next turn
547
+ // Include the last agent message as resultText for boss delegation processing.
548
+ // If an error item arrived in the same turn, append it so the boss/UI sees the failure.
549
+ if (this.lastAgentMessageText || this.lastErrorText) {
550
+ const parts = [];
551
+ if (this.lastAgentMessageText)
552
+ parts.push(this.lastAgentMessageText);
553
+ if (this.lastErrorText)
554
+ parts.push(`[Error] ${this.lastErrorText}`);
555
+ event.resultText = parts.join('\n\n');
556
+ this.lastAgentMessageText = undefined;
557
+ this.lastErrorText = undefined;
543
558
  }
544
559
  if (this.lastModelUsageSnapshot && (this.lastModelUsageSnapshot.contextWindow
545
560
  || this.lastModelUsageSnapshot.inputTokens !== undefined
@@ -16,6 +16,31 @@ if (!fs.existsSync(TEMP_DIR)) {
16
16
  }
17
17
  log.log(` Temp upload directory: ${TEMP_DIR}`);
18
18
  const router = Router();
19
+ /**
20
+ * Resolve a path query against an optional baseDir. Absolute paths pass through
21
+ * unchanged; relative paths are resolved via path.resolve(baseDir, rawPath).
22
+ * If no usable baseDir is supplied, the server's own cwd is used so file-modal
23
+ * links like `../../../tmp/foo.md` open even from contexts without an explicit
24
+ * agent cwd (e.g. spotlight, flat view). The client should pass the agent cwd
25
+ * as baseDir whenever it has one, which takes precedence.
26
+ */
27
+ export function resolveAndValidateFilePath(rawPath, baseDir, fallbackBaseDir = process.cwd()) {
28
+ if (!rawPath) {
29
+ return { ok: false, status: 400, error: 'Missing path parameter' };
30
+ }
31
+ if (path.isAbsolute(rawPath)) {
32
+ return { ok: true, path: rawPath };
33
+ }
34
+ const effectiveBase = baseDir && path.isAbsolute(baseDir) ? baseDir : fallbackBaseDir;
35
+ if (!path.isAbsolute(effectiveBase)) {
36
+ return {
37
+ ok: false,
38
+ status: 400,
39
+ error: 'Cannot resolve relative path: no absolute baseDir and server cwd is not absolute',
40
+ };
41
+ }
42
+ return { ok: true, path: path.resolve(effectiveBase, rawPath) };
43
+ }
19
44
  // Prevent browser from caching git-related GET responses (status, diff, branch, etc.)
20
45
  // Without this, browsers may serve stale cached data — e.g. deleted files still appearing.
21
46
  router.use('/git-*path', (_req, res, next) => {
@@ -26,23 +51,19 @@ router.use('/git-*path', (_req, res, next) => {
26
51
  // GET /api/files/read - Read file contents
27
52
  router.get('/read', async (req, res) => {
28
53
  try {
29
- const filePath = req.query.path;
30
- if (!filePath) {
31
- res.status(400).json({ error: 'Missing path parameter' });
32
- return;
33
- }
34
- // Security: ensure path is absolute and doesn't contain ..
35
- if (!path.isAbsolute(filePath)) {
36
- res.status(400).json({ error: 'Path must be absolute' });
54
+ const resolution = resolveAndValidateFilePath(req.query.path, req.query.baseDir);
55
+ if (!resolution.ok) {
56
+ res.status(resolution.status).json({ error: resolution.error });
37
57
  return;
38
58
  }
59
+ const filePath = resolution.path;
39
60
  if (!fs.existsSync(filePath)) {
40
- res.status(404).json({ error: 'File not found' });
61
+ res.status(404).json({ error: 'File not found', path: filePath });
41
62
  return;
42
63
  }
43
64
  const stats = fs.statSync(filePath);
44
65
  if (stats.isDirectory()) {
45
- res.status(400).json({ error: 'Path is a directory' });
66
+ res.status(400).json({ error: 'Path is a directory', path: filePath });
46
67
  return;
47
68
  }
48
69
  // Limit file size to 1MB
@@ -152,15 +173,12 @@ router.get('/resolve', async (req, res) => {
152
173
  // GET /api/files/exists - Check if a file exists
153
174
  router.get('/exists', async (req, res) => {
154
175
  try {
155
- const filePath = req.query.path;
156
- if (!filePath) {
157
- res.status(400).json({ error: 'Missing path parameter' });
158
- return;
159
- }
160
- if (!path.isAbsolute(filePath)) {
161
- res.status(400).json({ error: 'Path must be absolute' });
176
+ const resolution = resolveAndValidateFilePath(req.query.path, req.query.baseDir);
177
+ if (!resolution.ok) {
178
+ res.status(resolution.status).json({ error: resolution.error });
162
179
  return;
163
180
  }
181
+ const filePath = resolution.path;
164
182
  const exists = fs.existsSync(filePath);
165
183
  res.json({ exists, path: filePath });
166
184
  }
@@ -172,22 +190,19 @@ router.get('/exists', async (req, res) => {
172
190
  // GET /api/files/info - Get file info without content
173
191
  router.get('/info', async (req, res) => {
174
192
  try {
175
- const filePath = req.query.path;
176
- if (!filePath) {
177
- res.status(400).json({ error: 'Missing path parameter' });
178
- return;
179
- }
180
- if (!path.isAbsolute(filePath)) {
181
- res.status(400).json({ error: 'Path must be absolute' });
193
+ const resolution = resolveAndValidateFilePath(req.query.path, req.query.baseDir);
194
+ if (!resolution.ok) {
195
+ res.status(resolution.status).json({ error: resolution.error });
182
196
  return;
183
197
  }
198
+ const filePath = resolution.path;
184
199
  if (!fs.existsSync(filePath)) {
185
- res.status(404).json({ error: 'File not found' });
200
+ res.status(404).json({ error: 'File not found', path: filePath });
186
201
  return;
187
202
  }
188
203
  const stats = fs.statSync(filePath);
189
204
  if (stats.isDirectory()) {
190
- res.status(400).json({ error: 'Path is a directory' });
205
+ res.status(400).json({ error: 'Path is a directory', path: filePath });
191
206
  return;
192
207
  }
193
208
  const extension = path.extname(filePath).toLowerCase();
@@ -208,23 +223,20 @@ router.get('/info', async (req, res) => {
208
223
  // GET /api/files/binary - Read binary file (for images, PDFs, downloads)
209
224
  router.get('/binary', async (req, res) => {
210
225
  try {
211
- const filePath = req.query.path;
212
- const download = req.query.download === 'true';
213
- if (!filePath) {
214
- res.status(400).json({ error: 'Missing path parameter' });
215
- return;
216
- }
217
- if (!path.isAbsolute(filePath)) {
218
- res.status(400).json({ error: 'Path must be absolute' });
226
+ const resolution = resolveAndValidateFilePath(req.query.path, req.query.baseDir);
227
+ if (!resolution.ok) {
228
+ res.status(resolution.status).json({ error: resolution.error });
219
229
  return;
220
230
  }
231
+ const filePath = resolution.path;
232
+ const download = req.query.download === 'true';
221
233
  if (!fs.existsSync(filePath)) {
222
- res.status(404).json({ error: 'File not found' });
234
+ res.status(404).json({ error: 'File not found', path: filePath });
223
235
  return;
224
236
  }
225
237
  const stats = fs.statSync(filePath);
226
238
  if (stats.isDirectory()) {
227
- res.status(400).json({ error: 'Path is a directory' });
239
+ res.status(400).json({ error: 'Path is a directory', path: filePath });
228
240
  return;
229
241
  }
230
242
  // Limit file size to 50MB for binary files
@@ -280,18 +292,14 @@ router.get('/binary', async (req, res) => {
280
292
  // GET /api/files/list - List directory contents
281
293
  router.get('/list', async (req, res) => {
282
294
  try {
283
- const dirPath = req.query.path;
284
- if (!dirPath) {
285
- res.status(400).json({ error: 'Missing path parameter' });
286
- return;
287
- }
288
- // Security: ensure path is absolute
289
- if (!path.isAbsolute(dirPath)) {
290
- res.status(400).json({ error: 'Path must be absolute' });
295
+ const resolution = resolveAndValidateFilePath(req.query.path, req.query.baseDir);
296
+ if (!resolution.ok) {
297
+ res.status(resolution.status).json({ error: resolution.error });
291
298
  return;
292
299
  }
300
+ const dirPath = resolution.path;
293
301
  if (!fs.existsSync(dirPath)) {
294
- res.status(404).json({ error: 'Directory not found' });
302
+ res.status(404).json({ error: 'Directory not found', path: dirPath });
295
303
  return;
296
304
  }
297
305
  const stats = fs.statSync(dirPath);
@@ -420,18 +428,15 @@ function copyPathToDirectory(sourcePath, targetDir) {
420
428
  // GET /api/files/tree - Get recursive directory tree
421
429
  router.get('/tree', async (req, res) => {
422
430
  try {
423
- const dirPath = req.query.path;
424
- const maxDepth = parseInt(req.query.depth) || 5;
425
- if (!dirPath) {
426
- res.status(400).json({ error: 'Missing path parameter' });
427
- return;
428
- }
429
- if (!path.isAbsolute(dirPath)) {
430
- res.status(400).json({ error: 'Path must be absolute' });
431
+ const resolution = resolveAndValidateFilePath(req.query.path, req.query.baseDir);
432
+ if (!resolution.ok) {
433
+ res.status(resolution.status).json({ error: resolution.error });
431
434
  return;
432
435
  }
436
+ const dirPath = resolution.path;
437
+ const maxDepth = parseInt(req.query.depth) || 5;
433
438
  if (!fs.existsSync(dirPath)) {
434
- res.status(404).json({ error: 'Directory not found' });
439
+ res.status(404).json({ error: 'Directory not found', path: dirPath });
435
440
  return;
436
441
  }
437
442
  const stats = fs.statSync(dirPath);
@@ -1043,15 +1048,12 @@ router.post('/git-discard', async (req, res) => {
1043
1048
  // GET /api/files/git-original - Get original file content from git HEAD
1044
1049
  router.get('/git-original', async (req, res) => {
1045
1050
  try {
1046
- const filePath = req.query.path;
1047
- if (!filePath) {
1048
- res.status(400).json({ error: 'Missing path parameter' });
1049
- return;
1050
- }
1051
- if (!path.isAbsolute(filePath)) {
1052
- res.status(400).json({ error: 'Path must be absolute' });
1051
+ const resolution = resolveAndValidateFilePath(req.query.path, req.query.baseDir);
1052
+ if (!resolution.ok) {
1053
+ res.status(resolution.status).json({ error: resolution.error });
1053
1054
  return;
1054
1055
  }
1056
+ const filePath = resolution.path;
1055
1057
  // Find git root
1056
1058
  let gitRoot;
1057
1059
  try {
@@ -1105,15 +1107,12 @@ router.get('/git-original', async (req, res) => {
1105
1107
  // GET /api/files/git-diff - Get unified diff for a file
1106
1108
  router.get('/git-diff', async (req, res) => {
1107
1109
  try {
1108
- const filePath = req.query.path;
1109
- if (!filePath) {
1110
- res.status(400).json({ error: 'Missing path parameter' });
1111
- return;
1112
- }
1113
- if (!path.isAbsolute(filePath)) {
1114
- res.status(400).json({ error: 'Path must be absolute' });
1110
+ const resolution = resolveAndValidateFilePath(req.query.path, req.query.baseDir);
1111
+ if (!resolution.ok) {
1112
+ res.status(resolution.status).json({ error: resolution.error });
1115
1113
  return;
1116
1114
  }
1115
+ const filePath = resolution.path;
1117
1116
  // Find git root
1118
1117
  let gitRoot;
1119
1118
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tide-commander",
3
- "version": "1.78.0",
3
+ "version": "1.80.0",
4
4
  "description": "Visual multi-agent orchestrator and manager for Claude Code with 3D/2D interface",
5
5
  "repository": {
6
6
  "type": "git",