tide-commander 0.69.4 → 0.70.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/dist/index.html CHANGED
@@ -22,11 +22,11 @@
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-D-4CYuC3.js"></script>
25
+ <script type="module" crossorigin src="/assets/main-sCgooonZ.js"></script>
26
26
  <link rel="modulepreload" crossorigin href="/assets/modulepreload-polyfill-B5Qt9EMX.js">
27
27
  <link rel="modulepreload" crossorigin href="/assets/vendor-react-uS-d4TUT.js">
28
28
  <link rel="modulepreload" crossorigin href="/assets/vendor-three-DJ4p3FLF.js">
29
- <link rel="stylesheet" crossorigin href="/assets/main-Zi2pcuPE.css">
29
+ <link rel="stylesheet" crossorigin href="/assets/main-BjxM02Mr.css">
30
30
  </head>
31
31
  <body>
32
32
  <div id="app"></div>
@@ -206,7 +206,21 @@
206
206
  "drawToCreate": "Auf dem Schlachtfeld zeichnen um Bereiche zu erstellen",
207
207
  "rect": "Rechteck",
208
208
  "circle": "Kreis",
209
- "deleteArea": "Bereich löschen"
209
+ "deleteArea": "Bereich löschen",
210
+ "logo": "Logo",
211
+ "uploadLogo": "Logo hochladen",
212
+ "removeLogo": "Entfernen",
213
+ "logoPosition": "Position",
214
+ "logoSize": "Groesse",
215
+ "logoWidth": "B",
216
+ "logoHeight": "H",
217
+ "logoOpacity": "Deckkraft",
218
+ "keepAspectRatio": "Seitenverhaeltnis",
219
+ "posCenter": "Mitte",
220
+ "posTopLeft": "Oben-Links",
221
+ "posTopRight": "Oben-Rechts",
222
+ "posBottomLeft": "Unten-Links",
223
+ "posBottomRight": "Unten-Rechts"
210
224
  },
211
225
  "buildings": {
212
226
  "title": "Gebäude ({{count}})",
@@ -206,7 +206,21 @@
206
206
  "drawToCreate": "Draw on the battlefield to create areas",
207
207
  "rect": "Rect",
208
208
  "circle": "Circle",
209
- "deleteArea": "Delete area"
209
+ "deleteArea": "Delete area",
210
+ "logo": "Logo",
211
+ "uploadLogo": "Upload Logo",
212
+ "removeLogo": "Remove",
213
+ "logoPosition": "Position",
214
+ "logoSize": "Size",
215
+ "logoWidth": "W",
216
+ "logoHeight": "H",
217
+ "logoOpacity": "Opacity",
218
+ "keepAspectRatio": "Lock ratio",
219
+ "posCenter": "Center",
220
+ "posTopLeft": "Top-Left",
221
+ "posTopRight": "Top-Right",
222
+ "posBottomLeft": "Bot-Left",
223
+ "posBottomRight": "Bot-Right"
210
224
  },
211
225
  "buildings": {
212
226
  "title": "Buildings ({{count}})",
@@ -501,8 +501,12 @@
501
501
  "noResultsYet": "No query results yet.",
502
502
  "selectDbAndRun": "Select a database and run a query to see results.",
503
503
  "runQuery": "Run Query",
504
- "executeShortcut": "Execute query (Ctrl+Enter)",
504
+ "runAll": "Run All",
505
+ "runAtCursor": "Run at Cursor",
506
+ "executeShortcut": "Execute query at cursor (Ctrl+Enter)",
507
+ "executeCursorShortcut": "Execute all queries (Ctrl+Shift+Enter)",
505
508
  "pressCtrlEnter": "Press Ctrl + Enter to execute",
509
+ "pressCtrlEnterMulti": "Ctrl+Enter: Run at Cursor | Ctrl+Shift+Enter: Run All",
506
510
  "selectDbPlaceholder": "Select a database to start querying...",
507
511
  "enterQueryPlaceholder": "Enter your SQL query here...",
508
512
  "expandSidebar": "Expand sidebar",
@@ -206,7 +206,21 @@
206
206
  "drawToCreate": "Dibuja en el campo de batalla para crear areas",
207
207
  "rect": "Rect",
208
208
  "circle": "Circulo",
209
- "deleteArea": "Eliminar area"
209
+ "deleteArea": "Eliminar area",
210
+ "logo": "Logo",
211
+ "uploadLogo": "Subir Logo",
212
+ "removeLogo": "Eliminar",
213
+ "logoPosition": "Posicion",
214
+ "logoSize": "Tamano",
215
+ "logoWidth": "An",
216
+ "logoHeight": "Al",
217
+ "logoOpacity": "Opacidad",
218
+ "keepAspectRatio": "Mantener proporcion",
219
+ "posCenter": "Centro",
220
+ "posTopLeft": "Sup-Izq",
221
+ "posTopRight": "Sup-Der",
222
+ "posBottomLeft": "Inf-Izq",
223
+ "posBottomRight": "Inf-Der"
210
224
  },
211
225
  "buildings": {
212
226
  "title": "Edificios ({{count}})",
@@ -206,7 +206,21 @@
206
206
  "drawToCreate": "Dessinez sur le champ de bataille pour créer des zones",
207
207
  "rect": "Rect",
208
208
  "circle": "Cercle",
209
- "deleteArea": "Supprimer la zone"
209
+ "deleteArea": "Supprimer la zone",
210
+ "logo": "Logo",
211
+ "uploadLogo": "Importer le logo",
212
+ "removeLogo": "Supprimer",
213
+ "logoPosition": "Position",
214
+ "logoSize": "Taille",
215
+ "logoWidth": "L",
216
+ "logoHeight": "H",
217
+ "logoOpacity": "Opacite",
218
+ "keepAspectRatio": "Garder les proportions",
219
+ "posCenter": "Centre",
220
+ "posTopLeft": "Haut-Gauche",
221
+ "posTopRight": "Haut-Droite",
222
+ "posBottomLeft": "Bas-Gauche",
223
+ "posBottomRight": "Bas-Droite"
210
224
  },
211
225
  "buildings": {
212
226
  "title": "Bâtiments ({{count}})",
@@ -206,7 +206,21 @@
206
206
  "drawToCreate": "क्षेत्र बनाने के लिए रणभूमि पर ड्रॉ करें",
207
207
  "rect": "आयत",
208
208
  "circle": "वृत्त",
209
- "deleteArea": "क्षेत्र हटाएं"
209
+ "deleteArea": "क्षेत्र हटाएं",
210
+ "logo": "लोगो",
211
+ "uploadLogo": "लोगो अपलोड करें",
212
+ "removeLogo": "हटाएं",
213
+ "logoPosition": "स्थिति",
214
+ "logoSize": "आकार",
215
+ "logoWidth": "चौ",
216
+ "logoHeight": "ऊं",
217
+ "logoOpacity": "अपारदर्शिता",
218
+ "keepAspectRatio": "अनुपात बनाए रखें",
219
+ "posCenter": "केंद्र",
220
+ "posTopLeft": "ऊपर-बाएं",
221
+ "posTopRight": "ऊपर-दाएं",
222
+ "posBottomLeft": "नीचे-बाएं",
223
+ "posBottomRight": "नीचे-दाएं"
210
224
  },
211
225
  "buildings": {
212
226
  "title": "बिल्डिंग्स ({{count}})",
@@ -206,7 +206,21 @@
206
206
  "drawToCreate": "Disegna sul campo di battaglia per creare aree",
207
207
  "rect": "Rettangolo",
208
208
  "circle": "Cerchio",
209
- "deleteArea": "Elimina area"
209
+ "deleteArea": "Elimina area",
210
+ "logo": "Logo",
211
+ "uploadLogo": "Carica Logo",
212
+ "removeLogo": "Rimuovi",
213
+ "logoPosition": "Posizione",
214
+ "logoSize": "Dimensione",
215
+ "logoWidth": "L",
216
+ "logoHeight": "A",
217
+ "logoOpacity": "Opacita",
218
+ "keepAspectRatio": "Mantieni proporzioni",
219
+ "posCenter": "Centro",
220
+ "posTopLeft": "Alto-Sin",
221
+ "posTopRight": "Alto-Des",
222
+ "posBottomLeft": "Basso-Sin",
223
+ "posBottomRight": "Basso-Des"
210
224
  },
211
225
  "buildings": {
212
226
  "title": "Edifici ({{count}})",
@@ -206,7 +206,21 @@
206
206
  "drawToCreate": "フィールド上に描画してエリアを作成",
207
207
  "rect": "四角形",
208
208
  "circle": "円",
209
- "deleteArea": "エリアを削除"
209
+ "deleteArea": "エリアを削除",
210
+ "logo": "ロゴ",
211
+ "uploadLogo": "ロゴをアップロード",
212
+ "removeLogo": "削除",
213
+ "logoPosition": "位置",
214
+ "logoSize": "サイズ",
215
+ "logoWidth": "幅",
216
+ "logoHeight": "高",
217
+ "logoOpacity": "不透明度",
218
+ "keepAspectRatio": "縦横比を維持",
219
+ "posCenter": "中央",
220
+ "posTopLeft": "左上",
221
+ "posTopRight": "右上",
222
+ "posBottomLeft": "左下",
223
+ "posBottomRight": "右下"
210
224
  },
211
225
  "buildings": {
212
226
  "title": "ビルディング ({{count}})",
@@ -206,7 +206,21 @@
206
206
  "drawToCreate": "Desenhe no campo de batalha para criar areas",
207
207
  "rect": "Retangulo",
208
208
  "circle": "Circulo",
209
- "deleteArea": "Excluir area"
209
+ "deleteArea": "Excluir area",
210
+ "logo": "Logo",
211
+ "uploadLogo": "Enviar Logo",
212
+ "removeLogo": "Remover",
213
+ "logoPosition": "Posicao",
214
+ "logoSize": "Tamanho",
215
+ "logoWidth": "L",
216
+ "logoHeight": "A",
217
+ "logoOpacity": "Opacidade",
218
+ "keepAspectRatio": "Manter proporcao",
219
+ "posCenter": "Centro",
220
+ "posTopLeft": "Sup-Esq",
221
+ "posTopRight": "Sup-Dir",
222
+ "posBottomLeft": "Inf-Esq",
223
+ "posBottomRight": "Inf-Dir"
210
224
  },
211
225
  "buildings": {
212
226
  "title": "Edificios ({{count}})",
@@ -206,7 +206,21 @@
206
206
  "drawToCreate": "Нарисуйте на поле боя для создания зон",
207
207
  "rect": "Прямоугольник",
208
208
  "circle": "Круг",
209
- "deleteArea": "Удалить зону"
209
+ "deleteArea": "Удалить зону",
210
+ "logo": "Логотип",
211
+ "uploadLogo": "Загрузить логотип",
212
+ "removeLogo": "Удалить",
213
+ "logoPosition": "Позиция",
214
+ "logoSize": "Размер",
215
+ "logoWidth": "Ш",
216
+ "logoHeight": "В",
217
+ "logoOpacity": "Прозрачность",
218
+ "keepAspectRatio": "Сохранять пропорции",
219
+ "posCenter": "Центр",
220
+ "posTopLeft": "Верх-Лево",
221
+ "posTopRight": "Верх-Право",
222
+ "posBottomLeft": "Низ-Лево",
223
+ "posBottomRight": "Низ-Право"
210
224
  },
211
225
  "buildings": {
212
226
  "title": "Здания ({{count}})",
@@ -206,7 +206,21 @@
206
206
  "drawToCreate": "在战场上绘制以创建区域",
207
207
  "rect": "矩形",
208
208
  "circle": "圆形",
209
- "deleteArea": "删除区域"
209
+ "deleteArea": "删除区域",
210
+ "logo": "标志",
211
+ "uploadLogo": "上传标志",
212
+ "removeLogo": "移除",
213
+ "logoPosition": "位置",
214
+ "logoSize": "尺寸",
215
+ "logoWidth": "宽",
216
+ "logoHeight": "高",
217
+ "logoOpacity": "不透明度",
218
+ "keepAspectRatio": "锁定比例",
219
+ "posCenter": "居中",
220
+ "posTopLeft": "左上",
221
+ "posTopRight": "右上",
222
+ "posBottomLeft": "左下",
223
+ "posBottomRight": "右下"
210
224
  },
211
225
  "buildings": {
212
226
  "title": "建筑 ({{count}})",
@@ -27,6 +27,7 @@ const SKILLS_FILE = path.join(DATA_DIR, 'skills.json');
27
27
  const CUSTOM_CLASSES_FILE = path.join(DATA_DIR, 'custom-agent-classes.json');
28
28
  const RUNNING_PROCESSES_FILE = path.join(DATA_DIR, 'running-processes.json');
29
29
  const SECRETS_FILE = path.join(DATA_DIR, 'secrets.json');
30
+ const AREA_LOGOS_DIR = path.join(DATA_DIR, 'area-logos');
30
31
  // Maximum history entries per agent
31
32
  const MAX_HISTORY_PER_AGENT = 50;
32
33
  const MAX_DELEGATION_HISTORY_PER_BOSS = 100;
@@ -133,6 +134,30 @@ export function saveAreas(areas) {
133
134
  log.error(' Failed to save areas:', err);
134
135
  }
135
136
  }
137
+ /**
138
+ * Ensure area logos directory exists
139
+ */
140
+ export function ensureAreaLogosDir() {
141
+ if (!fs.existsSync(AREA_LOGOS_DIR)) {
142
+ fs.mkdirSync(AREA_LOGOS_DIR, { recursive: true });
143
+ }
144
+ }
145
+ /**
146
+ * Get path to the area logos directory
147
+ */
148
+ export function getAreaLogosDir() {
149
+ return AREA_LOGOS_DIR;
150
+ }
151
+ /**
152
+ * Delete an area logo file from disk
153
+ */
154
+ export function deleteAreaLogo(filename) {
155
+ const filePath = path.join(AREA_LOGOS_DIR, path.basename(filename));
156
+ if (fs.existsSync(filePath)) {
157
+ fs.unlinkSync(filePath);
158
+ log.log(` Deleted area logo: ${filename}`);
159
+ }
160
+ }
136
161
  /**
137
162
  * Update a single agent's session ID
138
163
  */
@@ -3,11 +3,137 @@
3
3
  * REST API endpoints for drawing/project areas
4
4
  */
5
5
  import { Router } from 'express';
6
- import { loadAreas } from '../data/index.js';
6
+ import * as fs from 'fs';
7
+ import * as path from 'path';
8
+ import * as crypto from 'crypto';
9
+ import { loadAreas, ensureAreaLogosDir, getAreaLogosDir, deleteAreaLogo } from '../data/index.js';
10
+ import { createLogger } from '../utils/logger.js';
11
+ const log = createLogger('Areas');
7
12
  const router = Router();
13
+ // Allowed image MIME types
14
+ const ALLOWED_IMAGE_TYPES = new Set([
15
+ 'image/png', 'image/jpeg', 'image/gif', 'image/webp', 'image/svg+xml', 'image/bmp',
16
+ ]);
17
+ // Max logo file size: 5MB
18
+ const MAX_LOGO_SIZE = 5 * 1024 * 1024;
8
19
  // GET /api/areas - List all drawing areas
9
20
  router.get('/', (_req, res) => {
10
21
  const areas = loadAreas();
11
22
  res.json(areas);
12
23
  });
24
+ // GET /api/areas/logos/:filename - Serve a logo image
25
+ router.get('/logos/:filename', (req, res) => {
26
+ try {
27
+ ensureAreaLogosDir();
28
+ const filename = path.basename(String(req.params.filename)); // sanitize
29
+ const filePath = path.join(getAreaLogosDir(), filename);
30
+ if (!fs.existsSync(filePath)) {
31
+ res.status(404).json({ error: 'Logo not found' });
32
+ return;
33
+ }
34
+ // Determine content type from extension
35
+ const ext = path.extname(filename).toLowerCase();
36
+ const mimeMap = {
37
+ '.png': 'image/png',
38
+ '.jpg': 'image/jpeg',
39
+ '.jpeg': 'image/jpeg',
40
+ '.gif': 'image/gif',
41
+ '.webp': 'image/webp',
42
+ '.svg': 'image/svg+xml',
43
+ '.bmp': 'image/bmp',
44
+ };
45
+ const contentType = mimeMap[ext] || 'application/octet-stream';
46
+ res.setHeader('Content-Type', contentType);
47
+ res.setHeader('Cache-Control', 'public, max-age=31536000'); // 1 year - filenames are unique
48
+ fs.createReadStream(filePath).pipe(res);
49
+ }
50
+ catch (err) {
51
+ log.error(' Failed to serve logo:', err);
52
+ res.status(500).json({ error: err.message });
53
+ }
54
+ });
55
+ // POST /api/areas/:areaId/logo - Upload a logo for a zone
56
+ router.post('/:areaId/logo', (req, res) => {
57
+ try {
58
+ ensureAreaLogosDir();
59
+ const { areaId } = req.params;
60
+ const contentType = req.headers['content-type'] || '';
61
+ // Validate image type
62
+ if (!ALLOWED_IMAGE_TYPES.has(contentType)) {
63
+ res.status(400).json({ error: `Invalid image type: ${contentType}. Allowed: ${[...ALLOWED_IMAGE_TYPES].join(', ')}` });
64
+ return;
65
+ }
66
+ // Delete existing logo for this area if any
67
+ const areas = loadAreas();
68
+ const area = areas.find(a => a.id === areaId);
69
+ if (area?.logo?.filename) {
70
+ deleteAreaLogo(area.logo.filename);
71
+ }
72
+ // Determine extension from content type
73
+ const extMap = {
74
+ 'image/png': '.png',
75
+ 'image/jpeg': '.jpg',
76
+ 'image/gif': '.gif',
77
+ 'image/webp': '.webp',
78
+ 'image/svg+xml': '.svg',
79
+ 'image/bmp': '.bmp',
80
+ };
81
+ const ext = extMap[contentType] || '.png';
82
+ const randomId = crypto.randomBytes(4).toString('hex');
83
+ const filename = `${areaId}-${randomId}${ext}`;
84
+ const filePath = path.join(getAreaLogosDir(), filename);
85
+ // Collect body data
86
+ const chunks = [];
87
+ let totalSize = 0;
88
+ req.on('data', (chunk) => {
89
+ totalSize += chunk.length;
90
+ if (totalSize > MAX_LOGO_SIZE) {
91
+ res.status(413).json({ error: `Logo too large. Max size: ${MAX_LOGO_SIZE / 1024 / 1024}MB` });
92
+ req.destroy();
93
+ return;
94
+ }
95
+ chunks.push(chunk);
96
+ });
97
+ req.on('end', () => {
98
+ if (res.headersSent)
99
+ return; // Already sent 413
100
+ const buffer = Buffer.concat(chunks);
101
+ fs.writeFileSync(filePath, buffer);
102
+ log.log(` Uploaded area logo: ${filename} (${buffer.length} bytes)`);
103
+ res.json({
104
+ success: true,
105
+ filename,
106
+ url: `/api/areas/logos/${filename}`,
107
+ size: buffer.length,
108
+ });
109
+ });
110
+ req.on('error', (err) => {
111
+ log.error(' Logo upload error:', err);
112
+ if (!res.headersSent) {
113
+ res.status(500).json({ error: 'Upload failed' });
114
+ }
115
+ });
116
+ }
117
+ catch (err) {
118
+ log.error(' Failed to upload logo:', err);
119
+ res.status(500).json({ error: err.message });
120
+ }
121
+ });
122
+ // DELETE /api/areas/:areaId/logo - Remove a logo from a zone
123
+ router.delete('/:areaId/logo', (_req, res) => {
124
+ try {
125
+ const { areaId } = _req.params;
126
+ const areas = loadAreas();
127
+ const area = areas.find(a => a.id === areaId);
128
+ if (area?.logo?.filename) {
129
+ deleteAreaLogo(area.logo.filename);
130
+ log.log(` Removed logo for area ${areaId}`);
131
+ }
132
+ res.json({ success: true });
133
+ }
134
+ catch (err) {
135
+ log.error(' Failed to delete logo:', err);
136
+ res.status(500).json({ error: err.message });
137
+ }
138
+ });
13
139
  export default router;
@@ -1,4 +1,4 @@
1
- import { saveAreas, saveBuildings } from '../../data/index.js';
1
+ import { saveAreas, saveBuildings, loadAreas, deleteAreaLogo } from '../../data/index.js';
2
2
  import { buildingService } from '../../services/index.js';
3
3
  import { logger } from '../../utils/index.js';
4
4
  const log = logger.ws;
@@ -11,6 +11,33 @@ function handleSyncMessage(ctx, payload, entityName, saveFn, updateType) {
11
11
  });
12
12
  }
13
13
  export function handleSyncAreas(ctx, payload) {
14
+ // Clean up orphaned logo files before saving
15
+ try {
16
+ const previousAreas = loadAreas();
17
+ const newAreaIds = new Set(payload.map(a => a.id));
18
+ for (const prev of previousAreas) {
19
+ // Area was deleted and had a logo
20
+ if (!newAreaIds.has(prev.id) && prev.logo?.filename) {
21
+ deleteAreaLogo(prev.logo.filename);
22
+ }
23
+ }
24
+ for (const area of payload) {
25
+ const prev = previousAreas.find(p => p.id === area.id);
26
+ if (!prev?.logo?.filename)
27
+ continue;
28
+ // Logo was removed from area
29
+ if (!area.logo?.filename) {
30
+ deleteAreaLogo(prev.logo.filename);
31
+ }
32
+ // Logo was replaced with a different file
33
+ else if (area.logo.filename !== prev.logo.filename) {
34
+ deleteAreaLogo(prev.logo.filename);
35
+ }
36
+ }
37
+ }
38
+ catch (err) {
39
+ log.error(' Failed to clean up area logos:', err);
40
+ }
14
41
  handleSyncMessage(ctx, payload, 'areas', saveAreas, 'areas_update');
15
42
  }
16
43
  export async function handleSyncBuildings(ctx, payload) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tide-commander",
3
- "version": "0.69.4",
3
+ "version": "0.70.0",
4
4
  "description": "Visual multi-agent orchestrator and manager for Claude Code with 3D/2D interface",
5
5
  "repository": {
6
6
  "type": "git",