tide-commander 0.83.0 → 0.84.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.
@@ -1 +1 @@
1
- import{W as s}from"./main-Bm_HUW5H.js";import"./modulepreload-polyfill-B5Qt9EMX.js";import"./vendor-react-uS-d4TUT.js";import"./vendor-three-DJ4p3FLF.js";class f 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{f as LocalNotificationsWeb};
1
+ import{W as s}from"./main-dSfKHXvf.js";import"./modulepreload-polyfill-B5Qt9EMX.js";import"./vendor-react-uS-d4TUT.js";import"./vendor-three-DJ4p3FLF.js";class f 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{f as LocalNotificationsWeb};
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-Bm_HUW5H.js"></script>
25
+ <script type="module" crossorigin src="/assets/main-dSfKHXvf.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-BefR5p6u.css">
29
+ <link rel="stylesheet" crossorigin href="/assets/main-C6IAMrFB.css">
30
30
  </head>
31
31
  <body>
32
32
  <div id="app"></div>
@@ -393,7 +393,11 @@
393
393
  "acceptCurrent": "Aktuellen akzeptieren",
394
394
  "acceptIncoming": "Eingehenden akzeptieren",
395
395
  "acceptAllCurrent": "Alle aktuellen akzeptieren",
396
+ "prevConflict": "Vorheriger Konflikt",
397
+ "nextConflict": "Nächster Konflikt",
398
+ "acceptBoth": "Beide akzeptieren",
396
399
  "acceptAllIncoming": "Alle eingehenden akzeptieren",
400
+ "acceptAllBoth": "Alle beide akzeptieren",
397
401
  "saving": "Wird gespeichert...",
398
402
  "saveAndResolve": "Speichern & als gelöst markieren",
399
403
  "closeMiddleClick": "Schließen (Mittelklick)",
@@ -312,6 +312,18 @@
312
312
  "clickToOpen": "Click to open",
313
313
  "updated": "(updated)"
314
314
  },
315
+ "onboarding": {
316
+ "title": "Welcome to Tide Commander",
317
+ "subtitle": "Your AI team is ready to deploy",
318
+ "step1Title": "Deploy an Agent",
319
+ "step1Desc": "Spawn a Claude Code or Codex agent pointed at any project directory.",
320
+ "step2Title": "Give it a Task",
321
+ "step2Desc": "Type a prompt and the agent works autonomously in its own terminal.",
322
+ "step3Title": "Scale Your Team",
323
+ "step3Desc": "Add more agents, assign bosses, and monitor everything in real time.",
324
+ "createFirst": "Create Your First Agent",
325
+ "explore": "Explore First"
326
+ },
315
327
  "floatingButtons": {
316
328
  "settingsAndTools": "Settings & Tools",
317
329
  "commanderView": "Commander View",
@@ -88,7 +88,8 @@
88
88
  "fog": "Fog",
89
89
  "brightness": "Brightness",
90
90
  "floor": "Floor",
91
- "sky": "Sky"
91
+ "sky": "Sky",
92
+ "battlefieldSize": "Battlefield Size"
92
93
  },
93
94
  "fogValues": {
94
95
  "off": "Off",
@@ -42,6 +42,7 @@
42
42
  "send": "Send",
43
43
  "attach": "Attach file",
44
44
  "attachOrPaste": "Attach file (or paste image)",
45
+ "dropToAttach": "Drop files to attach",
45
46
  "attachedFiles": "{{count}} file(s) attached",
46
47
  "removeAttachment": "Remove attachment",
47
48
  "pasteFilePath": "Paste a file path to attach",
@@ -393,7 +394,11 @@
393
394
  "acceptCurrent": "Accept Current",
394
395
  "acceptIncoming": "Accept Incoming",
395
396
  "acceptAllCurrent": "Accept All Current",
397
+ "prevConflict": "Previous Conflict",
398
+ "nextConflict": "Next Conflict",
399
+ "acceptBoth": "Accept Both",
396
400
  "acceptAllIncoming": "Accept All Incoming",
401
+ "acceptAllBoth": "Accept All Both",
397
402
  "saving": "Saving...",
398
403
  "saveAndResolve": "Save & Mark Resolved",
399
404
  "closeMiddleClick": "Close (Middle-click)",
@@ -393,7 +393,11 @@
393
393
  "acceptCurrent": "Aceptar actual",
394
394
  "acceptIncoming": "Aceptar entrante",
395
395
  "acceptAllCurrent": "Aceptar todo actual",
396
+ "prevConflict": "Conflicto anterior",
397
+ "nextConflict": "Conflicto siguiente",
398
+ "acceptBoth": "Aceptar ambos",
396
399
  "acceptAllIncoming": "Aceptar todo entrante",
400
+ "acceptAllBoth": "Aceptar todos ambos",
397
401
  "saving": "Guardando...",
398
402
  "saveAndResolve": "Guardar y marcar como resuelto",
399
403
  "closeMiddleClick": "Cerrar (clic medio)",
@@ -393,7 +393,11 @@
393
393
  "acceptCurrent": "Accepter l'actuel",
394
394
  "acceptIncoming": "Accepter l'entrant",
395
395
  "acceptAllCurrent": "Tout accepter (actuel)",
396
+ "prevConflict": "Conflit précédent",
397
+ "nextConflict": "Conflit suivant",
398
+ "acceptBoth": "Accepter les deux",
396
399
  "acceptAllIncoming": "Tout accepter (entrant)",
400
+ "acceptAllBoth": "Tout accepter (les deux)",
397
401
  "saving": "Enregistrement...",
398
402
  "saveAndResolve": "Enregistrer et marquer comme résolu",
399
403
  "closeMiddleClick": "Fermer (clic molette)",
@@ -393,7 +393,11 @@
393
393
  "acceptCurrent": "वर्तमान स्वीकार करें",
394
394
  "acceptIncoming": "आने वाली स्वीकार करें",
395
395
  "acceptAllCurrent": "सभी वर्तमान स्वीकार करें",
396
+ "prevConflict": "पिछला विवाद",
397
+ "nextConflict": "अगला विवाद",
398
+ "acceptBoth": "दोनों स्वीकार करें",
396
399
  "acceptAllIncoming": "सभी आने वाली स्वीकार करें",
400
+ "acceptAllBoth": "सभी दोनों स्वीकार करें",
397
401
  "saving": "सहेजा जा रहा है...",
398
402
  "saveAndResolve": "सहेजें और हल करें",
399
403
  "closeMiddleClick": "बंद करें (मिडिल-क्लिक)",
@@ -393,7 +393,11 @@
393
393
  "acceptCurrent": "Accetta corrente",
394
394
  "acceptIncoming": "Accetta in arrivo",
395
395
  "acceptAllCurrent": "Accetta tutti corrente",
396
+ "prevConflict": "Conflitto precedente",
397
+ "nextConflict": "Conflitto successivo",
398
+ "acceptBoth": "Accetta entrambi",
396
399
  "acceptAllIncoming": "Accetta tutti in arrivo",
400
+ "acceptAllBoth": "Accetta tutti entrambi",
397
401
  "saving": "Salvataggio...",
398
402
  "saveAndResolve": "Salva e segna come risolto",
399
403
  "closeMiddleClick": "Chiudi (clic centrale)",
@@ -393,7 +393,11 @@
393
393
  "acceptCurrent": "現在を採用",
394
394
  "acceptIncoming": "受信を採用",
395
395
  "acceptAllCurrent": "すべて現在を採用",
396
+ "prevConflict": "前の競合",
397
+ "nextConflict": "次の競合",
398
+ "acceptBoth": "両方を採用",
396
399
  "acceptAllIncoming": "すべて受信を採用",
400
+ "acceptAllBoth": "すべて両方を採用",
397
401
  "saving": "保存中...",
398
402
  "saveAndResolve": "保存して解決済みにする",
399
403
  "closeMiddleClick": "閉じる (中クリック)",
@@ -393,7 +393,11 @@
393
393
  "acceptCurrent": "Aceitar Atual",
394
394
  "acceptIncoming": "Aceitar Recebida",
395
395
  "acceptAllCurrent": "Aceitar Todas Atuais",
396
+ "prevConflict": "Conflito anterior",
397
+ "nextConflict": "Conflito seguinte",
398
+ "acceptBoth": "Aceitar Ambas",
396
399
  "acceptAllIncoming": "Aceitar Todas Recebidas",
400
+ "acceptAllBoth": "Aceitar Todas Ambas",
397
401
  "saving": "Salvando...",
398
402
  "saveAndResolve": "Salvar e Marcar como Resolvido",
399
403
  "closeMiddleClick": "Fechar (Clique do meio)",
@@ -393,7 +393,11 @@
393
393
  "acceptCurrent": "Принять текущую",
394
394
  "acceptIncoming": "Принять входящую",
395
395
  "acceptAllCurrent": "Принять все текущие",
396
+ "prevConflict": "Предыдущий конфликт",
397
+ "nextConflict": "Следующий конфликт",
398
+ "acceptBoth": "Принять оба",
396
399
  "acceptAllIncoming": "Принять все входящие",
400
+ "acceptAllBoth": "Принять все оба",
397
401
  "saving": "Сохранение...",
398
402
  "saveAndResolve": "Сохранить и отметить решённым",
399
403
  "closeMiddleClick": "Закрыть (средний клик)",
@@ -393,7 +393,11 @@
393
393
  "acceptCurrent": "接受当前",
394
394
  "acceptIncoming": "接受传入",
395
395
  "acceptAllCurrent": "接受所有当前",
396
+ "prevConflict": "上一个冲突",
397
+ "nextConflict": "下一个冲突",
398
+ "acceptBoth": "接受两者",
396
399
  "acceptAllIncoming": "接受所有传入",
400
+ "acceptAllBoth": "接受所有两者",
397
401
  "saving": "保存中...",
398
402
  "saveAndResolve": "保存并标记为已解决",
399
403
  "closeMiddleClick": "关闭(中键点击)",
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import { spawn } from 'node:child_process';
2
+ import { execSync, spawn } from 'node:child_process';
3
+ import { randomBytes } from 'node:crypto';
3
4
  import fs from 'node:fs';
4
5
  import os from 'node:os';
5
6
  import path from 'node:path';
@@ -11,6 +12,9 @@ const PID_FILE = path.join(PID_DIR, 'server.pid');
11
12
  const META_FILE = path.join(PID_DIR, 'server-meta.json');
12
13
  const LOG_FILE = path.join(process.cwd(), 'logs', 'server.log');
13
14
  const PACKAGE_NAME = 'tide-commander';
15
+ const TLS_DIR = path.join(os.homedir(), '.tide-commander', 'certs');
16
+ const DEFAULT_TLS_KEY_FILE = path.join(TLS_DIR, 'localhost-key.pem');
17
+ const DEFAULT_TLS_CERT_FILE = path.join(TLS_DIR, 'localhost.pem');
14
18
  function findProjectRoot(startDir) {
15
19
  let current = startDir;
16
20
  while (current !== path.dirname(current)) {
@@ -35,6 +39,15 @@ Options:
35
39
  -p, --port <port> Set server port (default: 6200)
36
40
  -H, --host <host> Set server host (default: 127.0.0.1)
37
41
  -l, --listen-all Listen on all network interfaces
42
+ --https Enable HTTPS/WSS server mode
43
+ --tls-key <path> TLS private key path (PEM)
44
+ --tls-cert <path> TLS certificate path (PEM)
45
+ --install-local-cert
46
+ Install local trusted cert with mkcert
47
+ --auth-token <token>
48
+ Set AUTH_TOKEN for this server run
49
+ --generate-auth-token
50
+ Generate a secure AUTH_TOKEN automatically
38
51
  -f, --foreground Run in foreground (default is background)
39
52
  --lines <n> Number of log lines for logs command (default: 100)
40
53
  --follow Follow logs stream (like tail -f)
@@ -79,6 +92,42 @@ function parseArgs(argv) {
79
92
  case '--listen-all':
80
93
  options.listenAll = true;
81
94
  break;
95
+ case '--https':
96
+ options.https = true;
97
+ break;
98
+ case '--tls-key': {
99
+ const value = argv[i + 1];
100
+ if (!value || value.startsWith('-')) {
101
+ throw new Error(`Missing value for ${arg}`);
102
+ }
103
+ options.tlsKey = value;
104
+ i += 1;
105
+ break;
106
+ }
107
+ case '--tls-cert': {
108
+ const value = argv[i + 1];
109
+ if (!value || value.startsWith('-')) {
110
+ throw new Error(`Missing value for ${arg}`);
111
+ }
112
+ options.tlsCert = value;
113
+ i += 1;
114
+ break;
115
+ }
116
+ case '--install-local-cert':
117
+ options.installLocalCert = true;
118
+ break;
119
+ case '--auth-token': {
120
+ const value = argv[i + 1];
121
+ if (!value || value.startsWith('-')) {
122
+ throw new Error(`Missing value for ${arg}`);
123
+ }
124
+ options.authToken = value;
125
+ i += 1;
126
+ break;
127
+ }
128
+ case '--generate-auth-token':
129
+ options.generateAuthToken = true;
130
+ break;
82
131
  case '-f':
83
132
  case '--foreground':
84
133
  if (options.command === 'logs') {
@@ -149,7 +198,9 @@ function readServerMeta() {
149
198
  const parsed = JSON.parse(raw);
150
199
  if (typeof parsed.pid === 'number'
151
200
  && typeof parsed.host === 'string'
152
- && typeof parsed.port === 'string') {
201
+ && typeof parsed.port === 'string'
202
+ && (parsed.https === undefined || typeof parsed.https === 'boolean')
203
+ && (parsed.authEnabled === undefined || typeof parsed.authEnabled === 'boolean')) {
153
204
  return parsed;
154
205
  }
155
206
  return null;
@@ -166,6 +217,42 @@ function clearServerMeta() {
166
217
  // no-op
167
218
  }
168
219
  }
220
+ function resolveFromCwd(filePath) {
221
+ if (filePath.startsWith('~/')) {
222
+ return path.join(os.homedir(), filePath.slice(2));
223
+ }
224
+ if (path.isAbsolute(filePath)) {
225
+ return filePath;
226
+ }
227
+ return path.resolve(process.cwd(), filePath);
228
+ }
229
+ function ensureFileExists(filePath, label) {
230
+ if (!fs.existsSync(filePath)) {
231
+ throw new Error(`${label} not found: ${filePath}`);
232
+ }
233
+ }
234
+ function installLocalCert(host) {
235
+ fs.mkdirSync(TLS_DIR, { recursive: true });
236
+ const mkcertCmd = 'mkcert -install';
237
+ try {
238
+ spawnSyncOrThrow(mkcertCmd);
239
+ }
240
+ catch {
241
+ throw new Error('mkcert is required for --install-local-cert');
242
+ }
243
+ const hostArgs = ['localhost', '127.0.0.1', '::1'];
244
+ if (host !== 'localhost' && host !== '127.0.0.1' && host !== '::1' && host !== '0.0.0.0' && host !== '::') {
245
+ hostArgs.push(host);
246
+ }
247
+ const mkcertGenCmd = `mkcert -cert-file "${DEFAULT_TLS_CERT_FILE}" -key-file "${DEFAULT_TLS_KEY_FILE}" ${hostArgs.join(' ')}`;
248
+ spawnSyncOrThrow(mkcertGenCmd);
249
+ ensureFileExists(DEFAULT_TLS_CERT_FILE, 'TLS cert');
250
+ ensureFileExists(DEFAULT_TLS_KEY_FILE, 'TLS key');
251
+ return { keyPath: DEFAULT_TLS_KEY_FILE, certPath: DEFAULT_TLS_CERT_FILE };
252
+ }
253
+ function spawnSyncOrThrow(command) {
254
+ execSync(command, { stdio: 'ignore' });
255
+ }
169
256
  function readPidFile() {
170
257
  try {
171
258
  const raw = fs.readFileSync(PID_FILE, 'utf8').trim();
@@ -277,13 +364,16 @@ async function statusCommand() {
277
364
  const meta = readServerMeta();
278
365
  const port = meta?.port ?? process.env.PORT ?? '6200';
279
366
  const host = meta?.host ?? process.env.HOST ?? 'localhost';
280
- const url = `http://${host}:${port}`;
367
+ const protocol = meta?.https ? 'https' : 'http';
368
+ const url = `${protocol}://${host}:${port}`;
369
+ const authEnabled = meta?.authEnabled === true;
281
370
  const uptime = getProcessUptime(pid);
282
371
  const version = getPackageVersion();
283
372
  console.log(`\n${cyan}${bright}🌊 Tide Commander Status${reset}`);
284
373
  console.log(`${cyan}${'═'.repeat(60)}${reset}`);
285
374
  console.log(`${green}✓ Running${reset} (PID: ${pid})`);
286
375
  console.log(`${blue}${bright}🚀 Access: ${url}${reset}`);
376
+ console.log(` Auth: ${authEnabled ? 'enabled' : 'disabled'}`);
287
377
  console.log(` Version: ${version}`);
288
378
  const npmVersion = await checkNpmVersion(PACKAGE_NAME, version);
289
379
  if (npmVersion.relation === 'behind' && npmVersion.latestVersion) {
@@ -415,6 +505,9 @@ function printUpdateNotice(latestVersion) {
415
505
  const dim = '\x1b[2m';
416
506
  console.log(`${yellow}${bright}⬆ Update available: v${latestVersion}${reset} ${dim}(run: bunx tide-commander@latest)${reset}`);
417
507
  }
508
+ function generateAuthToken() {
509
+ return randomBytes(32).toString('hex');
510
+ }
418
511
  function versionCommand() {
419
512
  try {
420
513
  const version = getPackageVersion();
@@ -454,6 +547,47 @@ async function main() {
454
547
  process.env.HOST = '0.0.0.0';
455
548
  process.env.LISTEN_ALL_INTERFACES = '1';
456
549
  }
550
+ if (options.tlsKey && !options.tlsCert) {
551
+ throw new Error('--tls-key requires --tls-cert');
552
+ }
553
+ if (options.tlsCert && !options.tlsKey) {
554
+ throw new Error('--tls-cert requires --tls-key');
555
+ }
556
+ if (options.generateAuthToken && options.authToken) {
557
+ throw new Error('--generate-auth-token cannot be used with --auth-token');
558
+ }
559
+ const shouldEnableHttps = options.https === true
560
+ || options.installLocalCert === true
561
+ || options.tlsKey !== undefined
562
+ || options.tlsCert !== undefined
563
+ || process.env.HTTPS === '1';
564
+ if (shouldEnableHttps) {
565
+ process.env.HTTPS = '1';
566
+ }
567
+ if (options.installLocalCert) {
568
+ const host = process.env.HOST || 'localhost';
569
+ const generated = installLocalCert(host);
570
+ process.env.TLS_KEY_PATH = generated.keyPath;
571
+ process.env.TLS_CERT_PATH = generated.certPath;
572
+ }
573
+ if (options.tlsKey && options.tlsCert) {
574
+ process.env.TLS_KEY_PATH = resolveFromCwd(options.tlsKey);
575
+ process.env.TLS_CERT_PATH = resolveFromCwd(options.tlsCert);
576
+ }
577
+ if (process.env.HTTPS === '1') {
578
+ const tlsKeyPath = resolveFromCwd(process.env.TLS_KEY_PATH || DEFAULT_TLS_KEY_FILE);
579
+ const tlsCertPath = resolveFromCwd(process.env.TLS_CERT_PATH || DEFAULT_TLS_CERT_FILE);
580
+ ensureFileExists(tlsKeyPath, 'TLS key');
581
+ ensureFileExists(tlsCertPath, 'TLS cert');
582
+ process.env.TLS_KEY_PATH = tlsKeyPath;
583
+ process.env.TLS_CERT_PATH = tlsCertPath;
584
+ }
585
+ if (options.generateAuthToken) {
586
+ process.env.AUTH_TOKEN = generateAuthToken();
587
+ }
588
+ else if (options.authToken) {
589
+ process.env.AUTH_TOKEN = options.authToken;
590
+ }
457
591
  const cliDir = path.dirname(fileURLToPath(import.meta.url));
458
592
  const serverLaunch = resolveServerLaunch(cliDir);
459
593
  const runInForeground = options.foreground === true || process.env.TIDE_COMMANDER_FOREGROUND === '1';
@@ -461,6 +595,12 @@ async function main() {
461
595
  const hasStartupOverrides = options.port !== undefined
462
596
  || options.host !== undefined
463
597
  || options.listenAll === true
598
+ || options.https === true
599
+ || options.tlsKey !== undefined
600
+ || options.tlsCert !== undefined
601
+ || options.installLocalCert === true
602
+ || options.authToken !== undefined
603
+ || options.generateAuthToken === true
464
604
  || options.foreground === true;
465
605
  if (existingPid && isRunning(existingPid)) {
466
606
  if (hasStartupOverrides) {
@@ -483,7 +623,9 @@ async function main() {
483
623
  const meta = readServerMeta();
484
624
  const port = meta?.port ?? process.env.PORT ?? '6200';
485
625
  const host = meta?.host ?? process.env.HOST ?? 'localhost';
486
- const url = `http://${host}:${port}`;
626
+ const protocol = meta?.https ? 'https' : 'http';
627
+ const url = `${protocol}://${host}:${port}`;
628
+ const authEnabled = meta?.authEnabled === true;
487
629
  const dim = '\x1b[2m';
488
630
  const yellow = '\x1b[33m';
489
631
  const cyan = '\x1b[36m';
@@ -495,6 +637,7 @@ async function main() {
495
637
  console.log(`\n${cyan}${bright}🌊 Tide Commander${reset} ${dim}(already running, PID: ${existingPid})${reset}`);
496
638
  console.log(`${cyan}${'═'.repeat(60)}${reset}`);
497
639
  console.log(`${blue}${bright}🚀 Open: ${url}${reset}`);
640
+ console.log(` Auth: ${authEnabled ? 'enabled' : 'disabled'}`);
498
641
  console.log(` Version: ${currentVer}`);
499
642
  const npmVersion = await checkNpmVersion(PACKAGE_NAME, currentVer);
500
643
  if (npmVersion.relation === 'behind' && npmVersion.latestVersion) {
@@ -537,12 +680,16 @@ async function main() {
537
680
  pid: child.pid,
538
681
  host: process.env.HOST || 'localhost',
539
682
  port: process.env.PORT || '6200',
683
+ https: process.env.HTTPS === '1',
684
+ authEnabled: Boolean(process.env.AUTH_TOKEN),
540
685
  });
541
686
  }
542
687
  child.unref();
543
688
  const port = process.env.PORT || '6200';
544
689
  const host = process.env.HOST || 'localhost';
545
- const url = `http://${host}:${port}`;
690
+ const protocol = process.env.HTTPS === '1' ? 'https' : 'http';
691
+ const url = `${protocol}://${host}:${port}`;
692
+ const authEnabled = Boolean(process.env.AUTH_TOKEN);
546
693
  // ANSI color codes for beautiful output
547
694
  const cyan = '\x1b[36m';
548
695
  const green = '\x1b[32m';
@@ -556,7 +703,11 @@ async function main() {
556
703
  const currentVersion = getPackageVersion();
557
704
  console.log(`${green}✓${reset} Started in background (PID: ${child.pid ?? 'unknown'})`);
558
705
  console.log(`${blue}${bright}🚀 Open: ${url}${reset}`);
706
+ console.log(` Auth: ${authEnabled ? 'enabled' : 'disabled'}`);
559
707
  console.log(` Version: ${currentVersion}`);
708
+ if (options.generateAuthToken && process.env.AUTH_TOKEN) {
709
+ console.log(` Generated AUTH_TOKEN: ${process.env.AUTH_TOKEN}`);
710
+ }
560
711
  const npmVersion = await checkNpmVersion(PACKAGE_NAME, currentVersion);
561
712
  if (npmVersion.relation === 'behind' && npmVersion.latestVersion) {
562
713
  printUpdateNotice(npmVersion.latestVersion);
@@ -578,6 +729,8 @@ async function main() {
578
729
  pid: child.pid,
579
730
  host: process.env.HOST || 'localhost',
580
731
  port: process.env.PORT || '6200',
732
+ https: process.env.HTTPS === '1',
733
+ authEnabled: Boolean(process.env.AUTH_TOKEN),
581
734
  });
582
735
  }
583
736
  child.on('exit', (code, signal) => {
@@ -14,6 +14,7 @@ import { bitbucketPR } from './bitbucket-pr.js';
14
14
  import { pm2Logs } from './pm2-logs.js';
15
15
  import { createBuilding } from './create-building.js';
16
16
  import { releasePipeline } from './release-pipeline.js';
17
+ import { taskLabel } from './task-label.js';
17
18
  /**
18
19
  * All built-in skills that ship with Tide Commander
19
20
  */
@@ -27,6 +28,7 @@ export const BUILTIN_SKILLS = [
27
28
  pm2Logs,
28
29
  createBuilding,
29
30
  releasePipeline,
31
+ taskLabel,
30
32
  ];
31
33
  /**
32
34
  * Get the ID for a built-in skill based on its slug
@@ -0,0 +1,29 @@
1
+ export const taskLabel = {
2
+ slug: 'task-label',
3
+ name: 'Task Label',
4
+ description: 'Generate a brief task label for scene display',
5
+ allowedTools: ['Bash(curl:*)'],
6
+ assignedAgentClasses: ['*'],
7
+ content: `# Task Label (MANDATORY - Execute FIRST)
8
+
9
+ **IMPORTANT: Before doing ANY other work, you MUST generate a brief task label describing your current task.**
10
+
11
+ ## Steps (do this FIRST before anything else):
12
+ 1. Read the user's request
13
+ 2. Generate a 1-5 word summary of the task (e.g., "Fix auth bug", "Add dark mode", "Refactor API calls", "Update tests")
14
+ 3. Call the API to set your task label:
15
+
16
+ \`\`\`bash
17
+ curl -s -X PATCH http://localhost:5174/api/agents/YOUR_AGENT_ID -H "Content-Type: application/json" -d '{"taskLabel":"YOUR 1-5 WORD LABEL"}'
18
+ \`\`\`
19
+
20
+ 4. Then proceed with the actual task
21
+
22
+ ## Rules
23
+ - Keep labels between 1-5 words maximum
24
+ - Use action verbs: Fix, Add, Update, Refactor, Debug, Implement, Remove, etc.
25
+ - Be specific but concise (e.g., "Fix login redirect" not "Work on stuff")
26
+ - Do this BEFORE any other work - it should be your very first action
27
+ - Do NOT use exclamation marks (!) in the label - they cause bash errors
28
+ - Replace YOUR_AGENT_ID with your actual agent ID from the system prompt`,
29
+ };
@@ -132,6 +132,7 @@ function toStoredAgents(agents) {
132
132
  currentTask: agent.currentTask,
133
133
  lastAssignedTask: agent.lastAssignedTask,
134
134
  lastAssignedTaskTime: agent.lastAssignedTaskTime,
135
+ taskLabel: agent.taskLabel,
135
136
  isBoss: agent.isBoss,
136
137
  subordinateIds: agent.subordinateIds,
137
138
  bossId: agent.bossId,
@@ -4,6 +4,8 @@
4
4
  */
5
5
  import 'dotenv/config';
6
6
  import { createServer } from 'http';
7
+ import { createServer as createHttpsServer } from 'https';
8
+ import fs from 'node:fs';
7
9
  import { createApp } from './app.js';
8
10
  import { agentService, runtimeService, supervisorService, bossService, skillService, customClassService, secretsService, buildingService } from './services/index.js';
9
11
  import * as websocket from './websocket/handler.js';
@@ -12,6 +14,9 @@ import { logger, closeFileLogging, getLogFilePath } from './utils/logger.js';
12
14
  // Configuration
13
15
  const PORT = process.env.PORT || 6200;
14
16
  const HOST = process.env.HOST || (process.env.LISTEN_ALL_INTERFACES ? '::' : '127.0.0.1');
17
+ const HTTPS_ENABLED = process.env.HTTPS === '1';
18
+ const TLS_KEY_PATH = process.env.TLS_KEY_PATH;
19
+ const TLS_CERT_PATH = process.env.TLS_CERT_PATH;
15
20
  const FORCE_SHUTDOWN_TIMEOUT_MS = 4500;
16
21
  // ============================================================================
17
22
  // Global Error Handlers
@@ -57,7 +62,12 @@ async function main() {
57
62
  logger.server.log(`Log file: ${getLogFilePath()}`);
58
63
  // Create Express app and HTTP server
59
64
  const app = createApp();
60
- const server = createServer(app);
65
+ const server = HTTPS_ENABLED
66
+ ? createHttpsServer({
67
+ key: fs.readFileSync(assertTlsPath(TLS_KEY_PATH, 'TLS_KEY_PATH')),
68
+ cert: fs.readFileSync(assertTlsPath(TLS_CERT_PATH, 'TLS_CERT_PATH')),
69
+ }, app)
70
+ : createServer(app);
61
71
  const sockets = new Set();
62
72
  server.on('connection', (socket) => {
63
73
  sockets.add(socket);
@@ -83,9 +93,11 @@ async function main() {
83
93
  }
84
94
  });
85
95
  server.listen(Number(PORT), HOST, () => {
86
- logger.server.log(`Server running on http://${HOST}:${PORT}`);
87
- logger.server.log(`WebSocket available at ws://${HOST}:${PORT}/ws`);
88
- logger.server.log(`API available at http://${HOST}:${PORT}/api`);
96
+ const protocol = HTTPS_ENABLED ? 'https' : 'http';
97
+ const wsProtocol = HTTPS_ENABLED ? 'wss' : 'ws';
98
+ logger.server.log(`Server running on ${protocol}://${HOST}:${PORT}`);
99
+ logger.server.log(`WebSocket available at ${wsProtocol}://${HOST}:${PORT}/ws`);
100
+ logger.server.log(`API available at ${protocol}://${HOST}:${PORT}/api`);
89
101
  });
90
102
  let isShuttingDown = false;
91
103
  const gracefulShutdown = async (signal) => {
@@ -126,4 +138,10 @@ async function main() {
126
138
  process.on('SIGINT', () => { void gracefulShutdown('SIGINT'); });
127
139
  process.on('SIGTERM', () => { void gracefulShutdown('SIGTERM'); });
128
140
  }
141
+ function assertTlsPath(value, envName) {
142
+ if (!value) {
143
+ throw new Error(`${envName} is required when HTTPS=1`);
144
+ }
145
+ return value;
146
+ }
129
147
  main().catch(console.error);
@@ -24,6 +24,7 @@ export function createRuntimeCommandExecution(deps) {
24
24
  if (!isSystemMessage) {
25
25
  updateData.lastAssignedTask = command;
26
26
  updateData.lastAssignedTaskTime = Date.now();
27
+ updateData.taskLabel = undefined; // Clear for agent to regenerate via skill
27
28
  }
28
29
  if (Object.keys(updateData).length > 0) {
29
30
  agentService.updateAgent(agentId, updateData);
@@ -75,6 +76,7 @@ export function createRuntimeCommandExecution(deps) {
75
76
  if (!isSystemMessage) {
76
77
  updateData.lastAssignedTask = command;
77
78
  updateData.lastAssignedTaskTime = Date.now();
79
+ updateData.taskLabel = undefined; // Clear for agent to regenerate via skill
78
80
  }
79
81
  agentService.updateAgent(agentId, updateData);
80
82
  startStdinWatchdog({
@@ -250,8 +250,8 @@ export function getSkillsForAgent(agentId, agentClass) {
250
250
  // Check direct assignment
251
251
  if (skill.assignedAgentIds.includes(agentId))
252
252
  return true;
253
- // Check class assignment
254
- if (skill.assignedAgentClasses.includes(agentClass))
253
+ // Check class assignment (supports '*' wildcard for all classes)
254
+ if (skill.assignedAgentClasses.includes('*') || skill.assignedAgentClasses.includes(agentClass))
255
255
  return true;
256
256
  return false;
257
257
  });
@@ -432,8 +432,8 @@ async function handleSkillContentUpdate(skill) {
432
432
  // Check direct assignment
433
433
  if (skill.assignedAgentIds.includes(agent.id))
434
434
  return true;
435
- // Check class assignment
436
- if (skill.assignedAgentClasses.includes(agent.class))
435
+ // Check class assignment (supports '*' wildcard for all classes)
436
+ if (skill.assignedAgentClasses.includes('*') || skill.assignedAgentClasses.includes(agent.class))
437
437
  return true;
438
438
  return false;
439
439
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tide-commander",
3
- "version": "0.83.0",
3
+ "version": "0.84.0",
4
4
  "description": "Visual multi-agent orchestrator and manager for Claude Code with 3D/2D interface",
5
5
  "repository": {
6
6
  "type": "git",