tide-commander 1.94.0 → 1.96.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 (52) hide show
  1. package/dist/assets/{BossLogsModal-b4O2O00j.js → BossLogsModal-BFy6Rxdn.js} +1 -1
  2. package/dist/assets/{BossSpawnModal--Cn40tMQ.js → BossSpawnModal-KQyJYj_B.js} +1 -1
  3. package/dist/assets/{ControlsModal-SNJ95Qn9.js → ControlsModal-W_ghuG_Q.js} +1 -1
  4. package/dist/assets/{DockerLogsModal-DOkOsLaK.js → DockerLogsModal-valTcuRy.js} +1 -1
  5. package/dist/assets/{EmbeddedEditor-BKLJKK3B.js → EmbeddedEditor-CIsFWo6A.js} +1 -1
  6. package/dist/assets/{GmailOAuthSetup-B8ao6401.js → GmailOAuthSetup-AcW1zAlk.js} +1 -1
  7. package/dist/assets/{GoogleOAuthSetup-By-gup6S.js → GoogleOAuthSetup-C9uZ46Oe.js} +1 -1
  8. package/dist/assets/{IframeModal-KzrFyAG0.js → IframeModal-CmWv7GXl.js} +1 -1
  9. package/dist/assets/{IntegrationsPanel-CXMEldir.js → IntegrationsPanel-CNvzosgx.js} +2 -2
  10. package/dist/assets/{LogViewerModal-C70SfK7U.js → LogViewerModal-imTE-619.js} +1 -1
  11. package/dist/assets/{MonitoringModal-CbZtFYh9.js → MonitoringModal-DLxBY738.js} +1 -1
  12. package/dist/assets/{PM2LogsModal-C8mo6Tqu.js → PM2LogsModal-S4n0D11V.js} +1 -1
  13. package/dist/assets/{RestoreArchivedAreaModal-BUB-mruP.js → RestoreArchivedAreaModal-B32sqCxN.js} +1 -1
  14. package/dist/assets/{Scene2DCanvas-zinDgDAK.js → Scene2DCanvas-bEu0UPUm.js} +1 -1
  15. package/dist/assets/{SceneManager-CAxi1UHT.js → SceneManager-Ce3X0awy.js} +1 -1
  16. package/dist/assets/{SkillsPanel-Bh_J9suc.js → SkillsPanel-Dj22nDU8.js} +1 -1
  17. package/dist/assets/{SlackMultiInstanceSetup-DZSpM5ew.js → SlackMultiInstanceSetup-J9NdRY0U.js} +1 -1
  18. package/dist/assets/{SpawnModal-BrcKWjDs.js → SpawnModal-F9nspdWd.js} +1 -1
  19. package/dist/assets/{SubordinateAssignmentModal-CBqmszjc.js → SubordinateAssignmentModal-DT31CkPr.js} +1 -1
  20. package/dist/assets/TriggerManagerPanel-CVJ9ozHz.js +9 -0
  21. package/dist/assets/{WorkflowEditorPanel-DE9448gZ.js → WorkflowEditorPanel-BYRi0cU9.js} +1 -1
  22. package/dist/assets/{index-DuDcnBNX.js → index-B9G4hrFR.js} +1 -1
  23. package/dist/assets/{index-DPvgEcBT.js → index-BPy8NE5B.js} +3 -3
  24. package/dist/assets/{index-Cbk6B5BY.js → index-BmjrPptb.js} +1 -1
  25. package/dist/assets/{index-DelPL0HI.js → index-ByfAbkqv.js} +1 -1
  26. package/dist/assets/{index-B4VnKOHB.js → index-C5wC_Xis.js} +1 -1
  27. package/dist/assets/{index-Dk2THz0j.js → index-CLr8DpLX.js} +2 -2
  28. package/dist/assets/{index-BkZ422b_.js → index-DJm7oMXk.js} +1 -1
  29. package/dist/assets/{index-CpLZo6IM.js → index-DkjWoHhy.js} +1 -1
  30. package/dist/assets/{index-C7EieXis.js → index-DqZhHROd.js} +1 -1
  31. package/dist/assets/{main-CteALl_G.js → main-9uTEp9Lr.js} +4 -4
  32. package/dist/assets/{web-CGYGq80f.js → web-CLuvtqCm.js} +1 -1
  33. package/dist/assets/{web-BZa7rL0p.js → web-D-a3phls.js} +1 -1
  34. package/dist/assets/{web-DtevCxzm.js → web-DM2bFWBN.js} +1 -1
  35. package/dist/index.html +1 -1
  36. package/dist/src/packages/server/app.js +14 -0
  37. package/dist/src/packages/server/data/builtin-skills/bitbucket-pr-review.js +431 -0
  38. package/dist/src/packages/server/data/builtin-skills/bitbucket-reviewer.js +191 -0
  39. package/dist/src/packages/server/data/builtin-skills/create-building.js +521 -484
  40. package/dist/src/packages/server/data/builtin-skills/index.js +4 -0
  41. package/dist/src/packages/server/data/builtin-skills/trigger-designer.js +246 -71
  42. package/dist/src/packages/server/routes/bitbucket-author-loop.js +54 -0
  43. package/dist/src/packages/server/routes/buildings.js +298 -0
  44. package/dist/src/packages/server/routes/index.js +3 -1
  45. package/dist/src/packages/server/routes/trigger-routes.js +68 -15
  46. package/dist/src/packages/server/routes/webhook-dedupe.js +58 -0
  47. package/dist/src/packages/server/routes/webhook-signatures.js +67 -0
  48. package/dist/src/packages/server/services/building-service.js +400 -85
  49. package/dist/src/packages/server/services/trigger-service.js +5 -3
  50. package/dist/src/packages/server/websocket/handler.js +2 -4
  51. package/package.json +1 -1
  52. package/dist/assets/TriggerManagerPanel-BD7VnHYc.js +0 -9
@@ -6,7 +6,7 @@
6
6
  import { exec } from 'child_process';
7
7
  import { promisify } from 'util';
8
8
  import { loadBuildings, saveBuildings } from '../data/index.js';
9
- import { createLogger } from '../utils/index.js';
9
+ import { createLogger, generateId } from '../utils/index.js';
10
10
  import * as pm2Service from './pm2-service.js';
11
11
  import * as dockerService from './docker-service.js';
12
12
  import * as terminalService from './terminal-service.js';
@@ -939,6 +939,95 @@ function pollTerminalStatus(broadcast) {
939
939
  export function cleanupAllTerminals() {
940
940
  terminalService.cleanupAllTerminals();
941
941
  }
942
+ /**
943
+ * Reconcile runtime state for a single building when its config changes.
944
+ * Handles PM2 rename/restart, Docker rename/restart, and database tunnel teardown.
945
+ * Shared by handleBuildingSync (full sync from client) and updateBuilding (REST patch).
946
+ */
947
+ async function reconcileBuilding(oldBuilding, newBuilding) {
948
+ if (newBuilding.pm2?.enabled && oldBuilding.pm2?.enabled) {
949
+ const oldPM2Name = pm2Service.getPM2Name(oldBuilding);
950
+ const newPM2Name = pm2Service.getPM2Name(newBuilding);
951
+ const nameChanged = oldPM2Name !== newPM2Name;
952
+ const configChanged = hasPM2ConfigChanged(oldBuilding, newBuilding);
953
+ if (nameChanged || configChanged) {
954
+ const changeType = nameChanged ? 'name' : 'config';
955
+ log.log(`Building ${newBuilding.id}: PM2 ${changeType} changed`);
956
+ const oldStatus = await pm2Service.getStatus(oldBuilding);
957
+ const wasRunning = oldStatus?.status === 'online';
958
+ await pm2Service.deleteProcess(oldBuilding);
959
+ log.log(`Deleted old PM2 process: ${oldPM2Name}`);
960
+ if (wasRunning) {
961
+ const result = await pm2Service.startProcess(newBuilding);
962
+ if (result.success) {
963
+ log.log(`Started PM2 process with new ${changeType}: ${newPM2Name}`);
964
+ }
965
+ else {
966
+ log.error(`Failed to start PM2 process ${newPM2Name}: ${result.error}`);
967
+ }
968
+ }
969
+ }
970
+ }
971
+ // Database connection changes — tear down tunnels for connections that
972
+ // were removed or whose SSH config materially changed.
973
+ if (oldBuilding.type === 'database') {
974
+ const oldConns = oldBuilding.database?.connections || [];
975
+ const newConns = newBuilding.type === 'database'
976
+ ? (newBuilding.database?.connections || [])
977
+ : [];
978
+ const newConnsById = new Map(newConns.map(c => [c.id, c]));
979
+ for (const oldConn of oldConns) {
980
+ const fresh = newConnsById.get(oldConn.id);
981
+ if (!fresh) {
982
+ await databaseService.closeConnection(oldConn.id);
983
+ continue;
984
+ }
985
+ const oldKey = JSON.stringify({
986
+ ssh: oldConn.ssh ?? null,
987
+ host: oldConn.host,
988
+ port: oldConn.port,
989
+ });
990
+ const newKey = JSON.stringify({
991
+ ssh: fresh.ssh ?? null,
992
+ host: fresh.host,
993
+ port: fresh.port,
994
+ });
995
+ if (oldKey !== newKey) {
996
+ await databaseService.closeConnection(oldConn.id);
997
+ }
998
+ }
999
+ }
1000
+ if (newBuilding.docker?.enabled && oldBuilding.docker?.enabled) {
1001
+ const oldContainerName = dockerService.getContainerName(oldBuilding);
1002
+ const newContainerName = dockerService.getContainerName(newBuilding);
1003
+ const nameChanged = oldContainerName !== newContainerName;
1004
+ const configChanged = hasDockerConfigChanged(oldBuilding, newBuilding);
1005
+ if (nameChanged || configChanged) {
1006
+ const changeType = nameChanged ? 'name' : 'config';
1007
+ log.log(`Building ${newBuilding.id}: Docker ${changeType} changed`);
1008
+ const oldStatus = await dockerService.getStatus(oldBuilding);
1009
+ const wasRunning = oldStatus?.status === 'running';
1010
+ if (oldBuilding.docker.mode === 'compose') {
1011
+ await dockerService.composeDown(oldBuilding);
1012
+ }
1013
+ else {
1014
+ await dockerService.removeContainer(oldBuilding);
1015
+ }
1016
+ log.log(`Removed old Docker container: ${oldContainerName}`);
1017
+ if (wasRunning) {
1018
+ const result = newBuilding.docker.mode === 'compose'
1019
+ ? await dockerService.composeUp(newBuilding)
1020
+ : await dockerService.startContainer(newBuilding);
1021
+ if (result.success) {
1022
+ log.log(`Started Docker container with new ${changeType}: ${newContainerName}`);
1023
+ }
1024
+ else {
1025
+ log.error(`Failed to start Docker container ${newContainerName}: ${result.error}`);
1026
+ }
1027
+ }
1028
+ }
1029
+ }
1030
+ }
942
1031
  /**
943
1032
  * Handle building sync - detect PM2/Docker name/config changes and update processes
944
1033
  * Called when buildings are synced from the client
@@ -960,98 +1049,324 @@ export async function handleBuildingSync(newBuildings, _broadcast) {
960
1049
  }
961
1050
  for (const newBuilding of newBuildings) {
962
1051
  const oldBuilding = oldBuildingsMap.get(newBuilding.id);
963
- // Check if this is a PM2-enabled building
964
- if (oldBuilding && newBuilding.pm2?.enabled && oldBuilding.pm2?.enabled) {
965
- const oldPM2Name = pm2Service.getPM2Name(oldBuilding);
966
- const newPM2Name = pm2Service.getPM2Name(newBuilding);
967
- const nameChanged = oldPM2Name !== newPM2Name;
968
- const configChanged = hasPM2ConfigChanged(oldBuilding, newBuilding);
969
- log.log(`Building ${newBuilding.name}: nameChanged=${nameChanged}, configChanged=${configChanged}`);
970
- log.log(` Old args: "${oldBuilding.pm2?.args}", New args: "${newBuilding.pm2?.args}"`);
971
- log.log(` Old script: "${oldBuilding.pm2?.script}", New script: "${newBuilding.pm2?.script}"`);
972
- if (nameChanged || configChanged) {
973
- const changeType = nameChanged ? 'name' : 'config';
974
- log.log(`Building ${newBuilding.id}: PM2 ${changeType} changed`);
975
- // Check if the old process is running
976
- const oldStatus = await pm2Service.getStatus(oldBuilding);
977
- const wasRunning = oldStatus?.status === 'online';
978
- // Delete the old PM2 process
979
- await pm2Service.deleteProcess(oldBuilding);
980
- log.log(`Deleted old PM2 process: ${oldPM2Name}`);
981
- // If it was running, start the new one with updated config
982
- if (wasRunning) {
983
- const result = await pm2Service.startProcess(newBuilding);
984
- if (result.success) {
985
- log.log(`Started PM2 process with new ${changeType}: ${newPM2Name}`);
986
- }
987
- else {
988
- log.error(`Failed to start PM2 process ${newPM2Name}: ${result.error}`);
989
- }
990
- }
991
- }
1052
+ if (oldBuilding) {
1053
+ await reconcileBuilding(oldBuilding, newBuilding);
1054
+ }
1055
+ }
1056
+ }
1057
+ // ============================================================================
1058
+ // Validation & CRUD (Phase 1 - REST API support)
1059
+ // ============================================================================
1060
+ const VALID_TYPES = new Set([
1061
+ 'server', 'link', 'database', 'docker', 'monitor', 'folder', 'boss', 'terminal',
1062
+ ]);
1063
+ const VALID_STYLES = new Set([
1064
+ 'server-rack', 'tower', 'dome', 'pyramid', 'desktop',
1065
+ 'filing-cabinet', 'satellite', 'crystal', 'factory', 'command-center',
1066
+ ]);
1067
+ const VALID_INTERPRETERS = new Set([
1068
+ '', 'node', 'bun', 'python3', 'python', 'java', 'php', 'bash', 'none',
1069
+ ]);
1070
+ const VALID_DOCKER_MODES = new Set([
1071
+ 'container', 'compose', 'existing',
1072
+ ]);
1073
+ const VALID_DB_ENGINES = new Set([
1074
+ 'mysql', 'postgresql', 'oracle', 'sqlite', 'mssql',
1075
+ ]);
1076
+ const DEFAULT_STYLE_BY_TYPE = {
1077
+ server: 'server-rack',
1078
+ link: 'tower',
1079
+ database: 'dome',
1080
+ docker: 'crystal',
1081
+ monitor: 'satellite',
1082
+ folder: 'filing-cabinet',
1083
+ boss: 'command-center',
1084
+ terminal: 'desktop',
1085
+ };
1086
+ function generateBuildingId(name) {
1087
+ const slug = name
1088
+ .toLowerCase()
1089
+ .replace(/[^a-z0-9]+/g, '_')
1090
+ .replace(/^_+|_+$/g, '')
1091
+ .slice(0, 40);
1092
+ return `building_${Date.now()}_${slug || generateId()}`;
1093
+ }
1094
+ /**
1095
+ * Pure schema validator. Does not consult other buildings (cross-building
1096
+ * checks like dangling boss subordinates happen in createBuilding/updateBuilding).
1097
+ */
1098
+ export function validateBuilding(input) {
1099
+ const errors = [];
1100
+ if (typeof input.name !== 'string' || !input.name.trim()) {
1101
+ errors.push('name is required (non-empty string)');
1102
+ }
1103
+ if (!input.type || !VALID_TYPES.has(input.type)) {
1104
+ errors.push(`type must be one of: ${[...VALID_TYPES].join(', ')}`);
1105
+ }
1106
+ if (input.style && !VALID_STYLES.has(input.style)) {
1107
+ errors.push(`style must be one of: ${[...VALID_STYLES].join(', ')}`);
1108
+ }
1109
+ if (!input.position
1110
+ || typeof input.position.x !== 'number'
1111
+ || typeof input.position.z !== 'number') {
1112
+ errors.push('position is required ({x: number, z: number})');
1113
+ }
1114
+ if (input.pm2?.enabled) {
1115
+ if (!input.pm2.script || typeof input.pm2.script !== 'string') {
1116
+ errors.push('pm2.script is required when pm2.enabled is true');
1117
+ }
1118
+ if (input.pm2.interpreter !== undefined && !VALID_INTERPRETERS.has(input.pm2.interpreter)) {
1119
+ const allowed = [...VALID_INTERPRETERS].filter(i => i).join(', ');
1120
+ errors.push(`pm2.interpreter must be one of: ${allowed} (or "" for auto-detect)`);
992
1121
  }
993
- // Database connection changes — tear down tunnels for connections that
994
- // were removed or whose SSH config materially changed.
995
- if (oldBuilding && oldBuilding.type === 'database') {
996
- const oldConns = oldBuilding.database?.connections || [];
997
- const newConns = newBuilding.type === 'database'
998
- ? (newBuilding.database?.connections || [])
999
- : [];
1000
- const newConnsById = new Map(newConns.map(c => [c.id, c]));
1001
- for (const oldConn of oldConns) {
1002
- const fresh = newConnsById.get(oldConn.id);
1003
- if (!fresh) {
1004
- await databaseService.closeConnection(oldConn.id);
1005
- continue;
1122
+ }
1123
+ if (input.docker?.enabled) {
1124
+ if (!input.docker.mode || !VALID_DOCKER_MODES.has(input.docker.mode)) {
1125
+ errors.push(`docker.mode must be one of: ${[...VALID_DOCKER_MODES].join(', ')}`);
1126
+ }
1127
+ if (input.docker.mode === 'container' && !input.docker.image) {
1128
+ errors.push('docker.image is required when docker.mode is "container"');
1129
+ }
1130
+ if (input.docker.mode === 'compose' && !input.docker.composePath) {
1131
+ errors.push('docker.composePath is required when docker.mode is "compose"');
1132
+ }
1133
+ if (input.docker.mode === 'existing' && !input.docker.containerName) {
1134
+ errors.push('docker.containerName is required when docker.mode is "existing"');
1135
+ }
1136
+ }
1137
+ if (input.type === 'database') {
1138
+ const conns = input.database?.connections;
1139
+ if (!Array.isArray(conns) || conns.length === 0) {
1140
+ errors.push('database.connections must be a non-empty array for database buildings');
1141
+ }
1142
+ else {
1143
+ for (const conn of conns) {
1144
+ const label = conn.id || conn.name || '(unnamed)';
1145
+ if (!conn.id)
1146
+ errors.push(`database.connection ${label}: id is required`);
1147
+ if (!conn.name)
1148
+ errors.push(`database.connection ${label}: name is required`);
1149
+ if (!conn.engine || !VALID_DB_ENGINES.has(conn.engine)) {
1150
+ errors.push(`database.connection ${label}: engine must be one of ${[...VALID_DB_ENGINES].join(', ')}`);
1006
1151
  }
1007
- const oldKey = JSON.stringify({
1008
- ssh: oldConn.ssh ?? null,
1009
- host: oldConn.host,
1010
- port: oldConn.port,
1011
- });
1012
- const newKey = JSON.stringify({
1013
- ssh: fresh.ssh ?? null,
1014
- host: fresh.host,
1015
- port: fresh.port,
1016
- });
1017
- if (oldKey !== newKey) {
1018
- await databaseService.closeConnection(oldConn.id);
1152
+ if (conn.engine === 'sqlite') {
1153
+ if (!conn.filepath) {
1154
+ errors.push(`database.connection ${label}: filepath is required for sqlite`);
1155
+ }
1156
+ }
1157
+ else if (conn.engine) {
1158
+ if (!conn.host)
1159
+ errors.push(`database.connection ${label}: host is required for ${conn.engine}`);
1160
+ if (typeof conn.port !== 'number')
1161
+ errors.push(`database.connection ${label}: port must be a number for ${conn.engine}`);
1019
1162
  }
1020
1163
  }
1021
1164
  }
1022
- // Check if this is a Docker-enabled building
1023
- if (oldBuilding && newBuilding.docker?.enabled && oldBuilding.docker?.enabled) {
1024
- const oldContainerName = dockerService.getContainerName(oldBuilding);
1025
- const newContainerName = dockerService.getContainerName(newBuilding);
1026
- const nameChanged = oldContainerName !== newContainerName;
1027
- const configChanged = hasDockerConfigChanged(oldBuilding, newBuilding);
1028
- if (nameChanged || configChanged) {
1029
- const changeType = nameChanged ? 'name' : 'config';
1030
- log.log(`Building ${newBuilding.id}: Docker ${changeType} changed`);
1031
- // Check if the old container is running
1032
- const oldStatus = await dockerService.getStatus(oldBuilding);
1033
- const wasRunning = oldStatus?.status === 'running';
1034
- // Remove the old container
1035
- if (oldBuilding.docker.mode === 'compose') {
1036
- await dockerService.composeDown(oldBuilding);
1037
- }
1038
- else {
1039
- await dockerService.removeContainer(oldBuilding);
1165
+ }
1166
+ if (input.type === 'terminal' && !input.terminal?.enabled) {
1167
+ errors.push('terminal.enabled must be true for terminal buildings');
1168
+ }
1169
+ if (input.type === 'folder' && !input.folderPath) {
1170
+ errors.push('folderPath is required for folder buildings');
1171
+ }
1172
+ if (input.type === 'link' && (!input.urls || input.urls.length === 0)) {
1173
+ errors.push('urls is required (non-empty array) for link buildings');
1174
+ }
1175
+ return { ok: errors.length === 0, errors };
1176
+ }
1177
+ /**
1178
+ * Create a new building. Server assigns id, createdAt, and initial status.
1179
+ * Validates schema and cross-building references (boss subordinates).
1180
+ */
1181
+ export async function createBuilding(input, broadcast) {
1182
+ const validation = validateBuilding(input);
1183
+ if (!validation.ok) {
1184
+ return { ok: false, status: 400, errors: validation.errors };
1185
+ }
1186
+ const buildings = loadBuildings();
1187
+ if (input.type === 'boss' && input.subordinateBuildingIds?.length) {
1188
+ const existingIds = new Set(buildings.map(b => b.id));
1189
+ const dangling = input.subordinateBuildingIds.filter(id => !existingIds.has(id));
1190
+ if (dangling.length > 0) {
1191
+ return {
1192
+ ok: false,
1193
+ status: 400,
1194
+ errors: [`subordinateBuildingIds reference unknown buildings: ${dangling.join(', ')}`],
1195
+ };
1196
+ }
1197
+ }
1198
+ const now = Date.now();
1199
+ const building = {
1200
+ ...input,
1201
+ id: generateBuildingId(input.name),
1202
+ createdAt: now,
1203
+ lastActivity: now,
1204
+ status: 'stopped',
1205
+ style: input.style || DEFAULT_STYLE_BY_TYPE[input.type],
1206
+ };
1207
+ buildings.push(building);
1208
+ saveBuildings(buildings);
1209
+ broadcast({ type: 'building_created', payload: building });
1210
+ log.log(`Created building ${building.name} (${building.id})`);
1211
+ return { ok: true, building };
1212
+ }
1213
+ /**
1214
+ * Update an existing building. Strips server-managed fields from the patch,
1215
+ * runs the same validation as create, and reconciles runtime state
1216
+ * (PM2 / Docker / DB tunnels) if relevant fields changed.
1217
+ */
1218
+ export async function updateBuilding(id, patch, broadcast) {
1219
+ const buildings = loadBuildings();
1220
+ const idx = buildings.findIndex(b => b.id === id);
1221
+ if (idx === -1) {
1222
+ return { ok: false, status: 404, errors: [`Building not found: ${id}`] };
1223
+ }
1224
+ const oldBuilding = buildings[idx];
1225
+ // Strip server-managed fields from the patch — clients cannot change them.
1226
+ const safePatch = { ...patch };
1227
+ delete safePatch.id;
1228
+ delete safePatch.createdAt;
1229
+ delete safePatch.pm2Status;
1230
+ delete safePatch.dockerStatus;
1231
+ delete safePatch.terminalStatus;
1232
+ const newBuilding = {
1233
+ ...oldBuilding,
1234
+ ...safePatch,
1235
+ id: oldBuilding.id,
1236
+ createdAt: oldBuilding.createdAt,
1237
+ lastActivity: Date.now(),
1238
+ };
1239
+ const validation = validateBuilding(newBuilding);
1240
+ if (!validation.ok) {
1241
+ return { ok: false, status: 400, errors: validation.errors };
1242
+ }
1243
+ if (newBuilding.type === 'boss' && newBuilding.subordinateBuildingIds?.length) {
1244
+ const existingIds = new Set(buildings.map(b => b.id));
1245
+ const dangling = newBuilding.subordinateBuildingIds.filter(sid => !existingIds.has(sid));
1246
+ if (dangling.length > 0) {
1247
+ return {
1248
+ ok: false,
1249
+ status: 400,
1250
+ errors: [`subordinateBuildingIds reference unknown buildings: ${dangling.join(', ')}`],
1251
+ };
1252
+ }
1253
+ }
1254
+ buildings[idx] = newBuilding;
1255
+ saveBuildings(buildings);
1256
+ try {
1257
+ await reconcileBuilding(oldBuilding, newBuilding);
1258
+ }
1259
+ catch (err) {
1260
+ log.error(`Reconcile failed for building ${id}:`, err);
1261
+ }
1262
+ broadcast({ type: 'building_updated', payload: newBuilding });
1263
+ log.log(`Updated building ${newBuilding.name} (${newBuilding.id})`);
1264
+ return { ok: true, building: newBuilding };
1265
+ }
1266
+ /**
1267
+ * Delete a building. By default runs runtime cleanup (PM2 delete, Docker rm,
1268
+ * terminal teardown, DB tunnel close) before removing from the list.
1269
+ * Pass { cleanup: false } to skip cleanup (caller takes responsibility for orphan processes).
1270
+ * Also removes the deleted ID from any boss building's subordinate list.
1271
+ */
1272
+ export async function deleteBuilding(id, broadcast, opts = {}) {
1273
+ const buildings = loadBuildings();
1274
+ const idx = buildings.findIndex(b => b.id === id);
1275
+ if (idx === -1) {
1276
+ return { ok: false, status: 404, error: `Building not found: ${id}` };
1277
+ }
1278
+ const building = buildings[idx];
1279
+ if (opts.cleanup !== false) {
1280
+ try {
1281
+ await executeCommand(id, 'delete', broadcast);
1282
+ }
1283
+ catch (err) {
1284
+ log.error(`Cleanup during delete failed for ${building.name}:`, err);
1285
+ }
1286
+ if (building.type === 'database') {
1287
+ for (const conn of building.database?.connections || []) {
1288
+ try {
1289
+ await databaseService.closeConnection(conn.id);
1040
1290
  }
1041
- log.log(`Removed old Docker container: ${oldContainerName}`);
1042
- // If it was running, start the new one with updated config
1043
- if (wasRunning) {
1044
- const result = newBuilding.docker.mode === 'compose'
1045
- ? await dockerService.composeUp(newBuilding)
1046
- : await dockerService.startContainer(newBuilding);
1047
- if (result.success) {
1048
- log.log(`Started Docker container with new ${changeType}: ${newContainerName}`);
1049
- }
1050
- else {
1051
- log.error(`Failed to start Docker container ${newContainerName}: ${result.error}`);
1052
- }
1291
+ catch (err) {
1292
+ log.error(`Failed to close DB connection ${conn.id}:`, err);
1053
1293
  }
1054
1294
  }
1055
1295
  }
1056
1296
  }
1297
+ buildings.splice(idx, 1);
1298
+ // Drop dangling subordinate references in remaining boss buildings.
1299
+ for (const b of buildings) {
1300
+ if (b.type === 'boss' && b.subordinateBuildingIds?.includes(id)) {
1301
+ b.subordinateBuildingIds = b.subordinateBuildingIds.filter(sid => sid !== id);
1302
+ }
1303
+ }
1304
+ saveBuildings(buildings);
1305
+ broadcast({ type: 'building_deleted', payload: { id } });
1306
+ log.log(`Deleted building ${building.name} (${id})`);
1307
+ return { ok: true };
1308
+ }
1309
+ /**
1310
+ * Assign / replace the subordinates of a boss building.
1311
+ * Validates that the building is a boss and that all subordinate IDs exist.
1312
+ */
1313
+ export async function assignSubordinates(bossId, subordinateBuildingIds, broadcast) {
1314
+ const buildings = loadBuildings();
1315
+ const idx = buildings.findIndex(b => b.id === bossId);
1316
+ if (idx === -1) {
1317
+ return { ok: false, status: 404, errors: [`Building not found: ${bossId}`] };
1318
+ }
1319
+ if (buildings[idx].type !== 'boss') {
1320
+ return { ok: false, status: 400, errors: [`Building ${bossId} is not a boss building`] };
1321
+ }
1322
+ const existingIds = new Set(buildings.map(b => b.id));
1323
+ const dangling = subordinateBuildingIds.filter(sid => !existingIds.has(sid));
1324
+ if (dangling.length > 0) {
1325
+ return {
1326
+ ok: false,
1327
+ status: 400,
1328
+ errors: [`subordinateBuildingIds reference unknown buildings: ${dangling.join(', ')}`],
1329
+ };
1330
+ }
1331
+ buildings[idx] = {
1332
+ ...buildings[idx],
1333
+ subordinateBuildingIds: [...subordinateBuildingIds],
1334
+ lastActivity: Date.now(),
1335
+ };
1336
+ saveBuildings(buildings);
1337
+ broadcast({ type: 'building_updated', payload: buildings[idx] });
1338
+ return { ok: true, building: buildings[idx] };
1339
+ }
1340
+ /**
1341
+ * Snapshot logs for a building. Returns logs as a string and the source
1342
+ * ('pm2' | 'docker' | 'custom' | 'none') so the caller can label them.
1343
+ */
1344
+ export async function getBuildingLogs(id, lines = 200, service) {
1345
+ const building = getBuilding(id);
1346
+ if (!building) {
1347
+ return { ok: false, status: 404, error: `Building not found: ${id}` };
1348
+ }
1349
+ if (building.pm2?.enabled) {
1350
+ const logs = await pm2Service.getLogs(building, lines);
1351
+ return { ok: true, source: 'pm2', logs };
1352
+ }
1353
+ if (building.docker?.enabled) {
1354
+ const logs = await dockerService.getLogs(building, lines, service);
1355
+ return { ok: true, source: 'docker', logs };
1356
+ }
1357
+ const customLogsCmd = building.commands?.logs;
1358
+ if (customLogsCmd) {
1359
+ try {
1360
+ const { stdout, stderr } = await execAsync(customLogsCmd, {
1361
+ cwd: building.cwd,
1362
+ maxBuffer: 1024 * 1024,
1363
+ timeout: 15000,
1364
+ });
1365
+ return { ok: true, source: 'custom', logs: stdout + (stderr ? `\n${stderr}` : '') };
1366
+ }
1367
+ catch (err) {
1368
+ return { ok: true, source: 'custom', logs: `Error running logs command: ${err.message}` };
1369
+ }
1370
+ }
1371
+ return { ok: true, source: 'none', logs: '' };
1057
1372
  }
@@ -773,8 +773,10 @@ function createWebhookHandler() {
773
773
  // Nothing to stop
774
774
  },
775
775
  structuralMatch(trigger, _event) {
776
- // Webhook triggers always structurally match (the route already filters by triggerId)
777
- return trigger.type === 'webhook';
776
+ // Used as the fallback handler for both `webhook` and `bitbucket` triggers
777
+ // the central webhook receiver (POST /api/triggers/webhook/:id) already
778
+ // filtered by triggerId, so structural match is trivially true for either.
779
+ return trigger.type === 'webhook' || trigger.type === 'bitbucket';
778
780
  },
779
781
  extractVariables(trigger, event) {
780
782
  const variables = {
@@ -783,7 +785,7 @@ function createWebhookHandler() {
783
785
  };
784
786
  // Extract fields from payload if configured
785
787
  const extractFields = trigger.config.extractFields;
786
- if (trigger.type === 'webhook' && extractFields && event.data) {
788
+ if ((trigger.type === 'webhook' || trigger.type === 'bitbucket') && extractFields && event.data) {
787
789
  const payload = event.data;
788
790
  variables['payload'] = JSON.stringify(payload);
789
791
  for (const fieldPath of extractFields) {
@@ -6,7 +6,7 @@ import { WebSocketServer, WebSocket } from 'ws';
6
6
  import { agentService, bossMessageService, customClassService, permissionService, runtimeService, skillService, triggerService, workflowService, } from '../services/index.js';
7
7
  import { loadAreas, loadBuildings } from '../data/index.js';
8
8
  import { logger } from '../utils/index.js';
9
- import { setNotificationBroadcast, setExecBroadcast, setFocusAgentBroadcast, setAgentsBroadcast, setTriggerBroadcast } from '../routes/index.js';
9
+ import { setNotificationBroadcast, setExecBroadcast, setFocusAgentBroadcast, setAgentsBroadcast, setTriggerBroadcast, setBuildingsBroadcast } from '../routes/index.js';
10
10
  import { validateWebSocketAuth, isAuthEnabled } from '../auth/index.js';
11
11
  import { incrementWsSent, incrementWsReceived, setWsClientsCount } from '../routes/perf.js';
12
12
  import { handleSpawnAgent, handleKillAgent, handleStopAgent, handleClearContext, handleRestoreSession, handleRequestSessionHistory, handleCollapseContext, handleRequestContextStats, handleMoveAgent, handleRemoveAgent, handleRenameAgent, handleUpdateAgentProperties, handleCreateDirectory, handleReattachAgent, } from './handlers/agent-handler.js';
@@ -146,9 +146,6 @@ const messageHandlers = {
146
146
  update_agent_properties: handleUpdateAgentProperties,
147
147
  sync_areas: handleSyncAreas,
148
148
  sync_buildings: handleSyncBuildings,
149
- create_building: noopHandler,
150
- update_building: noopHandler,
151
- delete_building: noopHandler,
152
149
  building_command: handleBuildingCommand,
153
150
  pm2_logs_start: handlePM2LogsStart,
154
151
  pm2_logs_stop: handlePM2LogsStop,
@@ -304,6 +301,7 @@ export function init(server) {
304
301
  setFocusAgentBroadcast(broadcast);
305
302
  setAgentsBroadcast(broadcast);
306
303
  setTriggerBroadcast(broadcast);
304
+ setBuildingsBroadcast(broadcast);
307
305
  log.log('Handler initialized');
308
306
  return wss;
309
307
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tide-commander",
3
- "version": "1.94.0",
3
+ "version": "1.96.0",
4
4
  "description": "Visual multi-agent orchestrator and manager for Claude Code with 3D/2D interface",
5
5
  "repository": {
6
6
  "type": "git",
@@ -1,9 +0,0 @@
1
- import{ah as he,r as i,aJ as f,l as u,j as e}from"./main-CteALl_G.js";import"./vendor-react--Eh9ivFN.js";import"./vendor-three-Chj50gSY.js";const ue=[{value:"webhook",label:"Webhook"},{value:"cron",label:"Cron"},{value:"slack",label:"Slack"},{value:"email",label:"Email"},{value:"jira",label:"Jira"},{value:"whatsapp",label:"WhatsApp"}],ye=[{value:"structural",label:"Structural",desc:"Field-based matching (fast, free)"},{value:"llm",label:"LLM",desc:"Semantic matching via LLM (flexible, costs tokens)"},{value:"hybrid",label:"Hybrid",desc:"Structural pre-filter + LLM (recommended)"}],ge=[{value:"structural",label:"Structural"},{value:"llm",label:"LLM"}];function me(v){if(!v)return"";const d=new Date(v);if(Number.isNaN(d.getTime()))return"";const y=S=>String(S).padStart(2,"0");return`${d.getFullYear()}-${y(d.getMonth()+1)}-${y(d.getDate())}T${y(d.getHours())}:${y(d.getMinutes())}`}const A=()=>({name:"",description:"",type:"webhook",agentId:"",promptTemplate:"",enabled:!0,matchMode:"structural",extractionMode:"structural",config:{method:"POST"}});function ve({isOpen:v,onClose:d}){var _,$,J,V,G,X,Y,q,K,Q,Z,ee;const y=he(),[S,te]=i.useState([]),[T,w]=i.useState("list"),[n,b]=i.useState(A()),[c,E]=i.useState(!1),[z,L]=i.useState(!1),[B,p]=i.useState(null),[D,le]=i.useState([]),[k,se]=i.useState(null),[F,ae]=i.useState(`{
2
-
3
- }`),[r,C]=i.useState(null),[N,I]=i.useState(!1),[P,H]=i.useState([]),g=i.useCallback(async()=>{try{const t=await f(u("/api/triggers"));if(t.ok){const s=await t.json();te(s)}}catch(t){console.error("Failed to load triggers:",t)}},[]),M=i.useCallback(async t=>{try{const s=await f(u(`/api/triggers/${t}/events?limit=20`));if(s.ok){const a=await s.json();le(a)}}catch(s){console.error("Failed to load trigger history:",s)}},[]);i.useEffect(()=>{v&&g()},[v,g]),i.useEffect(()=>{k&&M(k)},[k,M]);const ne=i.useCallback(async()=>{L(!0),p(null);try{const t=c?u(`/api/triggers/${n.id}`):u("/api/triggers"),m=await f(t,{method:c?"PATCH":"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(n)});if(!m.ok){const j=await m.json();throw new Error(j.error||"Failed to save trigger")}await g(),w("list"),b(A()),E(!1)}catch(t){p(t instanceof Error?t.message:"Save failed")}finally{L(!1)}},[n,c,g]),ie=i.useCallback(async t=>{try{await f(u(`/api/triggers/${t}`),{method:"DELETE"}),await g()}catch(s){console.error("Failed to delete trigger:",s)}},[g]),re=i.useCallback(async t=>{try{await f(u(`/api/triggers/${t.id}`),{method:"PATCH",headers:{"Content-Type":"application/json"},body:JSON.stringify({enabled:!t.enabled})}),await g()}catch(s){console.error("Failed to toggle trigger:",s)}},[g]),oe=i.useCallback(async t=>{try{await f(u(`/api/triggers/${t}/fire`),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({variables:{}})}),k===t&&await M(t)}catch(s){console.error("Failed to test fire trigger:",s)}},[k,M]),de=i.useCallback(async()=>{if(n.id){I(!0),C(null);try{let t;try{t=JSON.parse(F)}catch{C({structuralMatch:void 0,extractedVariables:{},wouldFire:!1}),p("Invalid JSON payload");return}const s=await f(u(`/api/triggers/${n.id}/test-match`),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({payload:t})});if(s.ok){const a=await s.json();C(a),p(null)}else{const a=await s.json();p(a.error||"Test match failed")}}catch(t){p(t instanceof Error?t.message:"Test match failed")}finally{I(!1)}}},[n.id,F]),ce=i.useCallback(async t=>{var s;try{const a=await f(u("/api/triggers/validate-cron"),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({expression:t,timezone:((s=n.config)==null?void 0:s.timezone)||"UTC"})});if(a.ok){const m=await a.json();H(m.valid?m.nextFires:[])}}catch{H([])}},[n]),W=i.useCallback(t=>{t?(b({...t}),E(!0)):(b(A()),E(!1)),C(null),p(null),w("edit")},[]),o=i.useCallback((t,s)=>{b(a=>({...a,[t]:s}))},[]),x=i.useCallback((t,s)=>{b(a=>({...a,config:{...a.config,[t]:s}}))},[]),O=i.useCallback((t,s)=>{b(a=>({...a,llmMatch:{...a.llmMatch||{prompt:""},[t]:s}}))},[]),U=i.useCallback((t,s)=>{b(a=>({...a,llmExtract:{...a.llmExtract||{prompt:"",variables:[]},[t]:s}}))},[]),pe=i.useMemo(()=>Array.from(y.values()).map(t=>({id:t.id,name:t.name})),[y]);return v?e.jsx("div",{style:l.overlay,onClick:d,children:e.jsxs("div",{style:l.panel,onClick:t=>t.stopPropagation(),children:[e.jsxs("div",{style:l.header,children:[e.jsx("h2",{style:l.title,children:T==="list"?"Triggers":c?"Edit Trigger":"New Trigger"}),e.jsxs("div",{style:l.headerActions,children:[T==="list"&&e.jsx("button",{style:l.addBtn,onClick:()=>W(),children:"+ New Trigger"}),T==="edit"&&e.jsx("button",{style:l.backBtn,onClick:()=>{w("list"),C(null),p(null)},children:"Back"}),e.jsx("button",{style:l.closeBtn,onClick:d,children:"X"})]})]}),B&&e.jsx("div",{style:l.error,children:B}),T==="list"&&e.jsx("div",{style:l.content,children:S.length===0?e.jsx("div",{style:l.empty,children:"No triggers configured. Create one to get started."}):S.map(t=>{var s;return e.jsxs("div",{style:{...l.triggerCard,borderLeftColor:t.status==="error"?"#e74c3c":t.enabled?"#2ecc71":"#95a5a6"},onClick:()=>{se(t.id),W(t)},children:[e.jsxs("div",{style:l.cardHeader,children:[e.jsxs("div",{style:l.cardTitle,children:[e.jsx("span",{style:l.typeTag,children:t.type}),e.jsx("span",{children:t.name}),t.type==="cron"&&(()=>{const a=t.config||{};return a.runOnce?a.completedAt?e.jsx("span",{style:{...l.matchTag,color:"#a6e3a1"},children:"Once · completed"}):a.missedAt?e.jsx("span",{style:{...l.matchTag,color:"#f38ba8"},children:"Once · missed"}):e.jsx("span",{style:{...l.matchTag,color:"#f9e2af"},children:"Once · pending"}):e.jsx("span",{style:l.matchTag,children:"Recurring"})})(),t.matchMode!=="structural"&&e.jsx("span",{style:l.matchTag,children:t.matchMode})]}),e.jsxs("div",{style:l.cardActions,children:[e.jsx("button",{style:l.smallBtn,onClick:a=>{a.stopPropagation(),re(t)},children:t.enabled?"Disable":"Enable"}),e.jsx("button",{style:l.smallBtn,onClick:a=>{a.stopPropagation(),oe(t.id)},children:"Test Fire"}),e.jsx("button",{style:{...l.smallBtn,color:"#e74c3c"},onClick:a=>{a.stopPropagation(),ie(t.id)},children:"Delete"})]})]}),e.jsxs("div",{style:l.cardMeta,children:[e.jsxs("span",{children:["Agent: ",((s=y.get(t.agentId))==null?void 0:s.name)||t.agentId]}),e.jsxs("span",{children:["Fires: ",t.fireCount]}),t.lastFiredAt&&e.jsxs("span",{children:["Last: ",new Date(t.lastFiredAt).toLocaleString()]}),t.status==="error"&&t.lastError&&e.jsxs("span",{style:{color:"#e74c3c"},children:["Error: ",t.lastError]})]})]},t.id)})}),T==="edit"&&e.jsx("div",{style:l.content,children:e.jsxs("div",{style:l.form,children:[e.jsxs("div",{style:l.field,children:[e.jsx("label",{style:l.label,children:"Name"}),e.jsx("input",{style:l.input,value:n.name||"",onChange:t=>o("name",t.target.value),placeholder:"My Trigger"})]}),e.jsxs("div",{style:l.field,children:[e.jsx("label",{style:l.label,children:"Description"}),e.jsx("input",{style:l.input,value:n.description||"",onChange:t=>o("description",t.target.value),placeholder:"Optional description"})]}),e.jsxs("div",{style:l.row,children:[e.jsxs("div",{style:l.field,children:[e.jsx("label",{style:l.label,children:"Type"}),e.jsx("select",{style:l.select,value:n.type||"webhook",onChange:t=>{const s=t.target.value;o("type",s),s==="webhook"?o("config",{method:"POST"}):s==="cron"?o("config",{expression:"0 9 * * MON-FRI",timezone:"UTC"}):o("config",{}),(n.promptTemplate??"").trim()||(s==="slack"?o("promptTemplate",`Slack message from {{slack.user}} in {{slack.channelName}}:
4
- {{slack.message}}
5
-
6
- {{slack.attachmentsBlock}}`):s==="whatsapp"&&o("promptTemplate",`WhatsApp message from {{whatsapp.fromName}} ({{whatsapp.from}}):
7
- {{whatsapp.body}}
8
-
9
- {{whatsapp.attachmentsBlock}}`))},children:ue.map(t=>e.jsx("option",{value:t.value,children:t.label},t.value))})]}),e.jsxs("div",{style:l.field,children:[e.jsx("label",{style:l.label,children:"Agent"}),e.jsxs("select",{style:l.select,value:n.agentId||"",onChange:t=>o("agentId",t.target.value),children:[e.jsx("option",{value:"",children:"Select agent..."}),pe.map(t=>e.jsx("option",{value:t.id,children:t.name},t.id))]})]})]}),e.jsxs("div",{style:l.field,children:[e.jsx("label",{style:l.label,children:"Prompt Template"}),e.jsx("textarea",{style:l.textarea,value:n.promptTemplate||"",onChange:t=>o("promptTemplate",t.target.value),placeholder:"Message sent to agent. Use {{variable}} for interpolation.",rows:4})]}),n.type==="webhook"&&e.jsxs("div",{style:l.section,children:[e.jsx("h4",{style:l.sectionTitle,children:"Webhook Config"}),e.jsxs("div",{style:l.row,children:[e.jsxs("div",{style:l.field,children:[e.jsx("label",{style:l.label,children:"Method"}),e.jsxs("select",{style:l.select,value:((_=n.config)==null?void 0:_.method)||"POST",onChange:t=>x("method",t.target.value),children:[e.jsx("option",{value:"POST",children:"POST"}),e.jsx("option",{value:"PUT",children:"PUT"})]})]}),e.jsxs("div",{style:l.field,children:[e.jsx("label",{style:l.label,children:"Secret (HMAC)"}),e.jsx("input",{style:l.input,type:"password",value:(($=n.config)==null?void 0:$.secret)||"",onChange:t=>x("secret",t.target.value||void 0),placeholder:"Optional HMAC secret"})]})]}),e.jsxs("div",{style:l.field,children:[e.jsx("label",{style:l.label,children:"Extract Fields (comma-separated JSON paths)"}),e.jsx("input",{style:l.input,value:((V=(J=n.config)==null?void 0:J.extractFields)==null?void 0:V.join(", "))||"",onChange:t=>x("extractFields",t.target.value.split(",").map(s=>s.trim()).filter(Boolean)),placeholder:"body.release_name, body.version"})]}),c&&n.id&&e.jsxs("div",{style:l.webhookUrl,children:[e.jsx("label",{style:l.label,children:"Webhook URL"}),e.jsx("code",{style:l.code,children:`${window.location.origin}/api/triggers/webhook/${n.id}`})]})]}),n.type==="cron"&&(()=>{const t=n.config||{},s=!!t.runOnce,a=t.completedAt,m=t.missedAt,j=!!a;return e.jsxs("div",{style:l.section,children:[e.jsx("h4",{style:l.sectionTitle,children:"Cron Config"}),e.jsxs("div",{style:l.modeSelector,children:[e.jsxs("button",{type:"button",style:{...l.modeBtn,...s?{}:l.modeBtnActive},onClick:()=>x("runOnce",!1),disabled:j,children:[e.jsx("strong",{children:"Repeats"}),e.jsx("span",{style:l.modeDesc,children:"Fires on a cron schedule"})]}),e.jsxs("button",{type:"button",style:{...l.modeBtn,...s?l.modeBtnActive:{}},onClick:()=>x("runOnce",!0),disabled:j,children:[e.jsx("strong",{children:"Run once"}),e.jsx("span",{style:l.modeDesc,children:"Fires exactly once at a specific time"})]})]}),j&&e.jsxs("div",{style:{...l.testResult,borderLeft:"3px solid #a6e3a1"},children:[e.jsx("strong",{style:{color:"#a6e3a1"},children:"Completed"}),e.jsxs("span",{style:l.llmMeta,children:["Fired at ",new Date(a).toLocaleString()]})]}),!j&&m&&e.jsxs("div",{style:{...l.testResult,borderLeft:"3px solid #f38ba8"},children:[e.jsx("strong",{style:{color:"#f38ba8"},children:"Missed"}),e.jsxs("span",{style:l.llmMeta,children:["Marked missed at ",new Date(m).toLocaleString()," (server was down at runAt)"]})]}),s?e.jsx("div",{style:l.row,children:e.jsxs("div",{style:l.field,children:[e.jsx("label",{style:l.label,children:"Run at"}),e.jsx("input",{style:l.input,type:"datetime-local",value:me(t.runAt),onChange:h=>{const R=h.target.value;if(!R){x("runAt",void 0);return}const xe=new Date(R).toISOString();x("runAt",xe)},disabled:j}),e.jsx("span",{style:l.llmMeta,children:"Interpreted in your browser's local timezone"})]})}):e.jsxs("div",{style:l.row,children:[e.jsxs("div",{style:l.field,children:[e.jsx("label",{style:l.label,children:"Expression"}),e.jsx("input",{style:l.input,value:t.expression||"",onChange:h=>{x("expression",h.target.value),h.target.value&&ce(h.target.value)},placeholder:"0 9 * * MON-FRI"})]}),e.jsxs("div",{style:l.field,children:[e.jsx("label",{style:l.label,children:"Timezone"}),e.jsx("input",{style:l.input,value:t.timezone||"UTC",onChange:h=>x("timezone",h.target.value),placeholder:"America/Mexico_City"})]})]}),!s&&P.length>0&&e.jsxs("div",{style:l.nextFires,children:[e.jsx("label",{style:l.label,children:"Next fires:"}),P.map((h,R)=>e.jsx("div",{style:l.nextFireItem,children:new Date(h).toLocaleString()},R))]})]})})(),e.jsxs("div",{style:l.section,children:[e.jsx("h4",{style:l.sectionTitle,children:"Match Mode"}),e.jsx("div",{style:l.modeSelector,children:ye.map(t=>e.jsxs("button",{style:{...l.modeBtn,...n.matchMode===t.value?l.modeBtnActive:{}},onClick:()=>o("matchMode",t.value),children:[e.jsx("strong",{children:t.label}),e.jsx("span",{style:l.modeDesc,children:t.desc})]},t.value))}),(n.matchMode==="llm"||n.matchMode==="hybrid")&&e.jsxs("div",{style:l.llmConfig,children:[e.jsxs("div",{style:l.field,children:[e.jsx("label",{style:l.label,children:"LLM Match Prompt"}),e.jsx("textarea",{style:l.textarea,value:((G=n.llmMatch)==null?void 0:G.prompt)||"",onChange:t=>O("prompt",t.target.value),placeholder:"Does this message request a release deployment?",rows:3})]}),e.jsxs("div",{style:l.row,children:[e.jsxs("div",{style:l.field,children:[e.jsx("label",{style:l.label,children:"Model"}),e.jsxs("select",{style:l.select,value:((X=n.llmMatch)==null?void 0:X.model)||"haiku",onChange:t=>O("model",t.target.value),children:[e.jsx("option",{value:"haiku",children:"Haiku (fast, cheap)"}),e.jsx("option",{value:"sonnet",children:"Sonnet (balanced)"}),e.jsx("option",{value:"opus",children:"Opus (powerful)"})]})]}),e.jsxs("div",{style:l.field,children:[e.jsx("label",{style:l.label,children:"Min Confidence"}),e.jsx("input",{style:l.input,type:"number",min:"0",max:"1",step:"0.1",value:((Y=n.llmMatch)==null?void 0:Y.minConfidence)??0,onChange:t=>O("minConfidence",parseFloat(t.target.value))})]})]})]})]}),e.jsxs("div",{style:l.section,children:[e.jsx("h4",{style:l.sectionTitle,children:"Extraction Mode"}),e.jsx("div",{style:l.row,children:ge.map(t=>e.jsx("button",{style:{...l.modeBtn,...n.extractionMode===t.value?l.modeBtnActive:{}},onClick:()=>o("extractionMode",t.value),children:t.label},t.value))}),n.extractionMode==="llm"&&e.jsxs("div",{style:l.llmConfig,children:[e.jsxs("div",{style:l.field,children:[e.jsx("label",{style:l.label,children:"Extraction Instructions"}),e.jsx("textarea",{style:l.textarea,value:((q=n.llmExtract)==null?void 0:q.prompt)||"",onChange:t=>U("prompt",t.target.value),placeholder:"Extract: release version, affected systems, urgency",rows:3})]}),e.jsxs("div",{style:l.field,children:[e.jsx("label",{style:l.label,children:"Variable Names (comma-separated)"}),e.jsx("input",{style:l.input,value:((Q=(K=n.llmExtract)==null?void 0:K.variables)==null?void 0:Q.join(", "))||"",onChange:t=>U("variables",t.target.value.split(",").map(s=>s.trim()).filter(Boolean)),placeholder:"release_name, affected_systems, urgency"})]})]})]}),c&&e.jsxs("div",{style:l.section,children:[e.jsx("h4",{style:l.sectionTitle,children:"Test Match"}),e.jsxs("div",{style:l.field,children:[e.jsx("label",{style:l.label,children:"Sample Event Payload (JSON)"}),e.jsx("textarea",{style:l.textarea,value:F,onChange:t=>ae(t.target.value),rows:4})]}),e.jsx("button",{style:l.testBtn,onClick:de,disabled:N,children:N?"Testing...":"Test Match"}),r&&e.jsxs("div",{style:l.testResult,children:[e.jsx("div",{style:{...l.testResultHeader,color:r.wouldFire?"#2ecc71":"#e74c3c"},children:r.wouldFire?"WOULD FIRE":"WOULD NOT FIRE"}),r.structuralMatch!==void 0&&e.jsxs("div",{children:["Structural: ",r.structuralMatch?"Match":"No match"]}),r.llmMatch&&e.jsxs("div",{style:l.llmResult,children:[e.jsxs("div",{children:["LLM: ",r.llmMatch.match?"Match":"No match"]}),e.jsxs("div",{children:["Confidence: ",(r.llmMatch.confidence*100).toFixed(0),"%"]}),e.jsxs("div",{children:["Reason: ",r.llmMatch.reason]}),e.jsxs("div",{style:l.llmMeta,children:["Model: ",r.llmMatch.model," | Tokens: ",r.llmMatch.tokensUsed," | Latency: ",r.llmMatch.durationMs,"ms"]})]}),Object.keys(r.extractedVariables).length>0&&e.jsxs("div",{children:[e.jsx("div",{style:l.label,children:"Extracted Variables:"}),Object.entries(r.extractedVariables).map(([t,s])=>e.jsxs("div",{style:l.varRow,children:[e.jsx("code",{children:t}),": ",s]},t))]})]})]}),c&&D.length>0&&e.jsxs("div",{style:l.section,children:[e.jsx("h4",{style:l.sectionTitle,children:"Recent Fires"}),e.jsx("div",{style:l.historyList,children:D.map((t,s)=>{const a=t.llm_match_result?JSON.parse(t.llm_match_result):null;return e.jsxs("div",{style:l.historyItem,children:[e.jsxs("div",{style:l.historyHeader,children:[e.jsx("span",{style:{color:t.status==="delivered"?"#2ecc71":t.status==="failed"?"#e74c3c":"#f39c12"},children:t.status}),e.jsx("span",{children:new Date(t.fired_at).toLocaleString()}),t.duration_ms&&e.jsxs("span",{children:[t.duration_ms,"ms"]})]}),t.error&&e.jsx("div",{style:{color:"#e74c3c",fontSize:"12px"},children:t.error}),a&&e.jsxs("div",{style:l.llmMeta,children:["LLM: ",a.match?"match":"no match"," (",(a.confidence*100).toFixed(0),"%) - ",a.reason]})]},t.id||s)})})]}),e.jsxs("div",{style:l.actions,children:[e.jsx("button",{style:l.cancelBtn,onClick:()=>{w("list"),p(null)},children:"Cancel"}),e.jsx("button",{style:l.saveBtn,onClick:ne,disabled:z||!n.name||!n.agentId||n.type==="cron"&&!!((Z=n.config)!=null&&Z.runOnce)&&!((ee=n.config)!=null&&ee.runAt),children:z?"Saving...":c?"Update":"Create"})]})]})})]})}):null}const l={overlay:{position:"fixed",top:0,left:0,right:0,bottom:0,background:"rgba(0,0,0,0.6)",display:"flex",alignItems:"center",justifyContent:"center",zIndex:1e3},panel:{background:"#1e1e2e",borderRadius:"12px",width:"680px",maxHeight:"85vh",display:"flex",flexDirection:"column",color:"#cdd6f4",boxShadow:"0 8px 32px rgba(0,0,0,0.4)"},header:{display:"flex",justifyContent:"space-between",alignItems:"center",padding:"16px 20px",borderBottom:"1px solid #313244"},title:{margin:0,fontSize:"18px",fontWeight:600},headerActions:{display:"flex",gap:"8px"},content:{overflow:"auto",padding:"16px 20px",flex:1},empty:{textAlign:"center",padding:"40px",color:"#6c7086"},error:{background:"#45252a",color:"#f38ba8",padding:"8px 20px",fontSize:"13px"},triggerCard:{background:"#181825",borderRadius:"8px",padding:"12px 16px",marginBottom:"8px",borderLeft:"3px solid",cursor:"pointer",transition:"background 0.15s"},cardHeader:{display:"flex",justifyContent:"space-between",alignItems:"center"},cardTitle:{display:"flex",alignItems:"center",gap:"8px",fontWeight:500},typeTag:{background:"#313244",padding:"2px 6px",borderRadius:"4px",fontSize:"11px",textTransform:"uppercase"},matchTag:{background:"#45475a",padding:"2px 6px",borderRadius:"4px",fontSize:"10px",color:"#a6e3a1"},cardActions:{display:"flex",gap:"6px"},cardMeta:{display:"flex",gap:"16px",marginTop:"8px",fontSize:"12px",color:"#6c7086"},form:{display:"flex",flexDirection:"column",gap:"12px"},field:{display:"flex",flexDirection:"column",gap:"4px",flex:1},row:{display:"flex",gap:"12px"},label:{fontSize:"12px",color:"#a6adc8",fontWeight:500},input:{background:"#313244",border:"1px solid #45475a",borderRadius:"6px",padding:"8px 10px",color:"#cdd6f4",fontSize:"13px",outline:"none"},select:{background:"#313244",border:"1px solid #45475a",borderRadius:"6px",padding:"8px 10px",color:"#cdd6f4",fontSize:"13px",outline:"none"},textarea:{background:"#313244",border:"1px solid #45475a",borderRadius:"6px",padding:"8px 10px",color:"#cdd6f4",fontSize:"13px",outline:"none",fontFamily:"monospace",resize:"vertical"},section:{borderTop:"1px solid #313244",paddingTop:"12px"},sectionTitle:{margin:"0 0 8px",fontSize:"14px",fontWeight:600,color:"#a6adc8"},modeSelector:{display:"flex",gap:"8px",marginBottom:"8px"},modeBtn:{flex:1,background:"#313244",border:"1px solid #45475a",borderRadius:"6px",padding:"8px",color:"#cdd6f4",cursor:"pointer",textAlign:"left",display:"flex",flexDirection:"column",gap:"2px",fontSize:"12px"},modeBtnActive:{borderColor:"#89b4fa",background:"#1e3a5f"},modeDesc:{fontSize:"10px",color:"#6c7086"},llmConfig:{background:"#181825",borderRadius:"6px",padding:"12px",display:"flex",flexDirection:"column",gap:"8px"},webhookUrl:{marginTop:"8px"},code:{background:"#313244",padding:"6px 10px",borderRadius:"4px",fontSize:"12px",display:"block",wordBreak:"break-all",color:"#89b4fa"},nextFires:{marginTop:"8px"},nextFireItem:{fontSize:"12px",color:"#a6e3a1",padding:"2px 0"},testBtn:{background:"#89b4fa",border:"none",borderRadius:"6px",padding:"8px 16px",color:"#1e1e2e",fontWeight:600,cursor:"pointer",fontSize:"13px"},testResult:{background:"#181825",borderRadius:"6px",padding:"12px",marginTop:"8px",fontSize:"13px",display:"flex",flexDirection:"column",gap:"6px"},testResultHeader:{fontWeight:700,fontSize:"14px"},llmResult:{display:"flex",flexDirection:"column",gap:"2px"},llmMeta:{fontSize:"11px",color:"#6c7086",marginTop:"2px"},varRow:{fontSize:"12px",padding:"2px 0"},historyList:{display:"flex",flexDirection:"column",gap:"6px",maxHeight:"200px",overflow:"auto"},historyItem:{background:"#181825",borderRadius:"4px",padding:"8px 10px",fontSize:"12px"},historyHeader:{display:"flex",justifyContent:"space-between",alignItems:"center"},actions:{display:"flex",justifyContent:"flex-end",gap:"8px",marginTop:"16px",paddingTop:"12px",borderTop:"1px solid #313244"},addBtn:{background:"#89b4fa",border:"none",borderRadius:"6px",padding:"6px 14px",color:"#1e1e2e",fontWeight:600,cursor:"pointer",fontSize:"13px"},backBtn:{background:"transparent",border:"1px solid #45475a",borderRadius:"6px",padding:"6px 14px",color:"#cdd6f4",cursor:"pointer",fontSize:"13px"},closeBtn:{background:"transparent",border:"none",color:"#6c7086",cursor:"pointer",fontSize:"16px",padding:"4px 8px"},smallBtn:{background:"transparent",border:"1px solid #45475a",borderRadius:"4px",padding:"3px 8px",color:"#cdd6f4",cursor:"pointer",fontSize:"11px"},saveBtn:{background:"#a6e3a1",border:"none",borderRadius:"6px",padding:"8px 20px",color:"#1e1e2e",fontWeight:600,cursor:"pointer",fontSize:"13px"},cancelBtn:{background:"transparent",border:"1px solid #45475a",borderRadius:"6px",padding:"8px 20px",color:"#cdd6f4",cursor:"pointer",fontSize:"13px"}};export{ve as TriggerManagerPanel,ve as default};