tide-commander 1.95.0 → 1.97.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 (49) hide show
  1. package/dist/assets/{BossLogsModal-BNfB6g0E.js → BossLogsModal-CT25hD17.js} +1 -1
  2. package/dist/assets/{BossSpawnModal-BIcCbrjM.js → BossSpawnModal-9rS7AFkZ.js} +1 -1
  3. package/dist/assets/{ControlsModal-CS5jOEdY.js → ControlsModal-D-mymoM7.js} +1 -1
  4. package/dist/assets/{DockerLogsModal-B6sUNqY_.js → DockerLogsModal-Ae-ZCeeP.js} +1 -1
  5. package/dist/assets/EmbeddedEditor-DLOOpM0K.js +33 -0
  6. package/dist/assets/{GmailOAuthSetup-OPmwhJyE.js → GmailOAuthSetup-C9NLhWLo.js} +1 -1
  7. package/dist/assets/{GoogleOAuthSetup-BEc7lAua.js → GoogleOAuthSetup-1kzgrPV6.js} +1 -1
  8. package/dist/assets/{IframeModal-VrlQUMlO.js → IframeModal-DKS0IFsr.js} +1 -1
  9. package/dist/assets/{IntegrationsPanel-ta1yp-s4.js → IntegrationsPanel-CBvKOeud.js} +2 -2
  10. package/dist/assets/{LogViewerModal-B_4ke-1p.js → LogViewerModal-Dlt8JfVg.js} +1 -1
  11. package/dist/assets/{MonitoringModal-BHAuVdYA.js → MonitoringModal-BM1IEZv6.js} +1 -1
  12. package/dist/assets/{PM2LogsModal-BfKic3hD.js → PM2LogsModal-B1-HUHWZ.js} +1 -1
  13. package/dist/assets/{RestoreArchivedAreaModal-Bxk9GcPa.js → RestoreArchivedAreaModal-DXmYo7fp.js} +1 -1
  14. package/dist/assets/{Scene2DCanvas-COVSkNbV.js → Scene2DCanvas-CuUxSaPb.js} +1 -1
  15. package/dist/assets/{SceneManager-DWVI2idg.js → SceneManager-UD3IHY20.js} +1 -1
  16. package/dist/assets/{SkillsPanel-BnKqbJyg.js → SkillsPanel-DjRBVrO2.js} +1 -1
  17. package/dist/assets/SlackMultiInstanceSetup-Csp81Dqn.js +2 -0
  18. package/dist/assets/{SpawnModal-ClZUpgWy.js → SpawnModal-dg0mH3d9.js} +1 -1
  19. package/dist/assets/{SubordinateAssignmentModal-Dn8tejNU.js → SubordinateAssignmentModal-CeBPRNNX.js} +1 -1
  20. package/dist/assets/{TriggerManagerPanel-oBoHJdcv.js → TriggerManagerPanel-D1QPpFhP.js} +1 -1
  21. package/dist/assets/{WorkflowEditorPanel-Bwju9-46.js → WorkflowEditorPanel-IIsptZgp.js} +1 -1
  22. package/dist/assets/{index-Bu_n7vgB.js → index-BGh9tRSy.js} +1 -1
  23. package/dist/assets/{index-CfliOGe8.js → index-CIqkVLo1.js} +1 -1
  24. package/dist/assets/{index-CBh6qNCb.js → index-CNDUxsGy.js} +1 -1
  25. package/dist/assets/{index-umTVv-4x.js → index-CR9w26tq.js} +1 -1
  26. package/dist/assets/{index-DMUs4kjY.js → index-CsyPNc8u.js} +1 -1
  27. package/dist/assets/{index-DvBhO5je.js → index-DEI-vrXk.js} +1 -1
  28. package/dist/assets/{index-DgwVJN80.js → index-h-IcmGfB.js} +2 -2
  29. package/dist/assets/index-sDgBtEgH.js +19 -0
  30. package/dist/assets/{index-fIzifjgU.js → index-vJkimYqD.js} +1 -1
  31. package/dist/assets/main-BV_IuaBg.css +1 -0
  32. package/dist/assets/main-klWBzHh0.js +214 -0
  33. package/dist/assets/{web-Cp8n5FK3.js → web-BgPjNMBK.js} +1 -1
  34. package/dist/assets/{web-pfDqogx0.js → web-BmPSJLwQ.js} +1 -1
  35. package/dist/assets/{web-BD4VGICh.js → web-Dggt4D4N.js} +1 -1
  36. package/dist/index.html +2 -2
  37. package/dist/src/packages/server/data/builtin-skills/create-building.js +521 -484
  38. package/dist/src/packages/server/integrations/slack/slack-config.js +13 -0
  39. package/dist/src/packages/server/integrations/slack/slack-trigger-handler.js +12 -4
  40. package/dist/src/packages/server/routes/buildings.js +298 -0
  41. package/dist/src/packages/server/routes/index.js +3 -1
  42. package/dist/src/packages/server/services/building-service.js +400 -85
  43. package/dist/src/packages/server/websocket/handler.js +2 -4
  44. package/package.json +1 -1
  45. package/dist/assets/EmbeddedEditor-DiXmHZpX.js +0 -1
  46. package/dist/assets/SlackMultiInstanceSetup-BuA87Vlm.js +0 -2
  47. package/dist/assets/index-CXBrQLNP.js +0 -51
  48. package/dist/assets/main-BaGMbjuZ.js +0 -214
  49. package/dist/assets/main-BfT_95fk.css +0 -1
@@ -19,6 +19,7 @@ const DEFAULT_CONFIG = {
19
19
  pollingMinMsBetweenCalls: 1500,
20
20
  pollingUseSearch: false,
21
21
  mirrorOwnMessages: false,
22
+ reactOnTrigger: true,
22
23
  currentMode: 'none',
23
24
  };
24
25
  // ─── Config File Persistence ───
@@ -201,6 +202,14 @@ export const slackConfigSchema = [
201
202
  defaultValue: false,
202
203
  group: 'General',
203
204
  },
205
+ {
206
+ key: 'reactOnTrigger',
207
+ label: 'Auto-react with :eyes: on trigger',
208
+ type: 'boolean',
209
+ description: 'When a Slack trigger fires on an incoming message, react with 👀 as a visual acknowledgement. Default on. Per-instance — turn off for accounts where the reaction is noisy.',
210
+ defaultValue: true,
211
+ group: 'General',
212
+ },
204
213
  ];
205
214
  // ─── Per-Instance Secret Keys ───
206
215
  /**
@@ -243,6 +252,7 @@ export function getConfigValues(secrets, instanceId = DEFAULT_INSTANCE_ID) {
243
252
  pollingMinMsBetweenCalls: config.pollingMinMsBetweenCalls ?? 1500,
244
253
  pollingUseSearch: config.pollingUseSearch ?? false,
245
254
  mirrorOwnMessages: config.mirrorOwnMessages ?? false,
255
+ reactOnTrigger: config.reactOnTrigger ?? true,
246
256
  currentMode: config.currentMode ?? 'none',
247
257
  // Mask secret values for UI display
248
258
  SLACK_BOT_TOKEN: secrets.get(instanceSecretKey('SLACK_BOT_TOKEN', instanceId)) ? '********' : '',
@@ -296,6 +306,9 @@ export async function setConfigValues(values, secrets, instanceId = DEFAULT_INST
296
306
  if (typeof values.mirrorOwnMessages === 'boolean') {
297
307
  updates.mirrorOwnMessages = values.mirrorOwnMessages;
298
308
  }
309
+ if (typeof values.reactOnTrigger === 'boolean') {
310
+ updates.reactOnTrigger = values.reactOnTrigger;
311
+ }
299
312
  updateConfig(updates, instanceId);
300
313
  }
301
314
  // ─── Mode Detection ───
@@ -10,10 +10,15 @@
10
10
  import * as slackClient from './slack-client.js';
11
11
  import { listInstances } from './slack-instance.js';
12
12
  import { listInstanceMetas } from './slack-instance-manifest.js';
13
+ import { loadConfig } from './slack-config.js';
13
14
  import { formatAttachmentLine } from '../../services/attachment-downloader.js';
14
15
  const unsubscribers = [];
15
- /** Env toggle: set SLACK_REACT_ON_TRIGGER=false (or 0/no/off) to disable the auto-:eyes: ack. */
16
- function reactOnTriggerEnabled() {
16
+ /**
17
+ * Global kill-switch: `SLACK_REACT_ON_TRIGGER=false` (or 0/no/off) disables the
18
+ * auto-:eyes: ack across every instance regardless of per-instance config.
19
+ * Defaults to enabled when unset.
20
+ */
21
+ function envAllowsReact() {
17
22
  const raw = (process.env.SLACK_REACT_ON_TRIGGER ?? '').toLowerCase().trim();
18
23
  if (!raw)
19
24
  return true;
@@ -22,7 +27,7 @@ function reactOnTriggerEnabled() {
22
27
  export const slackTriggerHandler = {
23
28
  triggerType: 'slack',
24
29
  async startListening(onEvent) {
25
- const autoReact = reactOnTriggerEnabled();
30
+ const envOn = envAllowsReact();
26
31
  // Subscribe to every instance the manifest knows about. Unknown instances
27
32
  // (created later via the UI) are auto-created with `getInstance()` when
28
33
  // reconnect runs, but we re-subscribe on each integration shutdown/init
@@ -32,7 +37,10 @@ export const slackTriggerHandler = {
32
37
  const allInstances = listInstances().filter((i) => idSet.has(i.id));
33
38
  for (const inst of allInstances) {
34
39
  const off = inst.onMessage((message) => {
35
- if (autoReact) {
40
+ // Per-instance toggle is read fresh on each message so flipping the
41
+ // checkbox in the UI takes effect without restarting the trigger.
42
+ const perInstance = loadConfig(inst.id).reactOnTrigger ?? true;
43
+ if (envOn && perInstance) {
36
44
  // Use the instance-specific reaction so it posts as the right account.
37
45
  inst.addReaction({ channel: message.channel, ts: message.ts, name: 'eyes' })
38
46
  .catch(() => { });
@@ -0,0 +1,298 @@
1
+ /**
2
+ * Building Routes
3
+ * REST API endpoints for building lifecycle and control.
4
+ *
5
+ * This is the canonical surface for agents managing buildings. The
6
+ * underlying buildingService handles validation, ID generation,
7
+ * credential encryption, broadcast events, and PM2/Docker/Terminal
8
+ * reconciliation.
9
+ *
10
+ * Routes:
11
+ * GET /api/buildings - List buildings (secrets redacted)
12
+ * GET /api/buildings/:id - Get one building (secrets redacted)
13
+ * POST /api/buildings - Create a building
14
+ * PATCH /api/buildings/:id - Partial update
15
+ * DELETE /api/buildings/:id[?cleanup=false] - Delete (default: cleanup runtime artifacts)
16
+ *
17
+ * POST /api/buildings/:id/command - { command } start|stop|restart|healthCheck|logs|delete
18
+ * GET /api/buildings/:id/logs?lines=200&service=foo - Snapshot logs (PM2/Docker/custom)
19
+ * POST /api/buildings/:id/sync-status - Force PM2/Docker status refresh
20
+ * POST /api/buildings/:id/subordinates - Assign subordinates (boss only)
21
+ *
22
+ * POST /api/buildings/boss/:id/command - { command } start_all|stop_all|restart_all
23
+ *
24
+ * GET /api/buildings/docker/containers - List adoptable containers + compose projects
25
+ */
26
+ import { Router } from 'express';
27
+ import { buildingService } from '../services/index.js';
28
+ import * as dockerService from '../services/docker-service.js';
29
+ import * as terminalService from '../services/terminal-service.js';
30
+ import { broadcast } from '../websocket/handler.js';
31
+ import { createLogger } from '../utils/logger.js';
32
+ const log = createLogger('BuildingRoutes');
33
+ const router = Router();
34
+ let broadcastFn = null;
35
+ /**
36
+ * Wire the WebSocket broadcaster. Called from the websocket handler at startup.
37
+ * Falls back to the directly-imported broadcast() at call time if not wired,
38
+ * which lets these routes work in tests that don't initialise the WS layer.
39
+ */
40
+ export function setBroadcast(fn) {
41
+ broadcastFn = fn;
42
+ }
43
+ function emit(message) {
44
+ if (broadcastFn) {
45
+ broadcastFn(message);
46
+ }
47
+ else {
48
+ // In production setBroadcast() is always called from the WS handler init;
49
+ // this fallback keeps unit tests working without the full WS bootstrap.
50
+ try {
51
+ broadcast(message);
52
+ }
53
+ catch {
54
+ // No WS server up — broadcast is best-effort.
55
+ }
56
+ }
57
+ }
58
+ // ─── Helpers ──────────────────────────────────────────────────────────────────
59
+ const VALID_COMMANDS = new Set([
60
+ 'start', 'stop', 'restart', 'healthCheck', 'logs', 'delete',
61
+ ]);
62
+ const VALID_BOSS_COMMANDS = new Set([
63
+ 'start_all', 'stop_all', 'restart_all',
64
+ ]);
65
+ function redactDatabaseConnection(c) {
66
+ return {
67
+ id: c.id,
68
+ name: c.name,
69
+ engine: c.engine,
70
+ host: c.host,
71
+ port: c.port,
72
+ username: c.username,
73
+ database: c.database,
74
+ filepath: c.filepath,
75
+ ssl: c.ssl,
76
+ hasPassword: Boolean(c.password),
77
+ ssh: c.ssh
78
+ ? {
79
+ enabled: c.ssh.enabled,
80
+ host: c.ssh.host,
81
+ port: c.ssh.port,
82
+ username: c.ssh.username,
83
+ authMethod: c.ssh.authMethod,
84
+ hasPassword: Boolean(c.ssh.password),
85
+ hasPrivateKey: Boolean(c.ssh.privateKey || c.ssh.privateKeyPath),
86
+ }
87
+ : undefined,
88
+ };
89
+ }
90
+ /**
91
+ * Strip secret fields before returning a building over HTTP.
92
+ * Plaintext credentials may sit in memory after the agent creates a building,
93
+ * but they must never come back out of GET responses.
94
+ */
95
+ function redactBuilding(building) {
96
+ const out = { ...building };
97
+ if (building.database?.connections) {
98
+ out.database = {
99
+ ...building.database,
100
+ connections: building.database.connections.map(redactDatabaseConnection),
101
+ };
102
+ }
103
+ return out;
104
+ }
105
+ // ─── CRUD ─────────────────────────────────────────────────────────────────────
106
+ // GET /api/buildings — list all buildings (redacted)
107
+ router.get('/', (_req, res) => {
108
+ const buildings = buildingService.getBuildings().map(redactBuilding);
109
+ res.json({ buildings });
110
+ });
111
+ // GET /api/buildings/docker/containers — adoptable containers + compose projects
112
+ // (Must come before /:id so the docker prefix doesn't match as an id.)
113
+ router.get('/docker/containers', async (_req, res) => {
114
+ try {
115
+ const [containers, composeProjects] = await Promise.all([
116
+ dockerService.listAllContainers(),
117
+ dockerService.listComposeProjects(),
118
+ ]);
119
+ res.json({ containers, composeProjects });
120
+ }
121
+ catch (err) {
122
+ log.error('Failed to list Docker resources:', err);
123
+ res.status(500).json({ error: err.message });
124
+ }
125
+ });
126
+ // POST /api/buildings/boss/:id/command — body { command }
127
+ // Must come before /:id/command so 'boss' isn't matched as an id.
128
+ router.post('/boss/:id/command', async (req, res) => {
129
+ const command = req.body?.command;
130
+ if (!command || !VALID_BOSS_COMMANDS.has(command)) {
131
+ res.status(400).json({
132
+ error: `command must be one of: ${[...VALID_BOSS_COMMANDS].join(', ')}`,
133
+ });
134
+ return;
135
+ }
136
+ try {
137
+ const result = await buildingService.executeBossBuildingCommand(req.params.id, command, emit);
138
+ res.json(result);
139
+ }
140
+ catch (err) {
141
+ log.error(`Boss command failed for ${req.params.id}:`, err);
142
+ res.status(500).json({ error: err.message });
143
+ }
144
+ });
145
+ // GET /api/buildings/:id — fetch one (redacted)
146
+ router.get('/:id', (req, res) => {
147
+ const building = buildingService.getBuilding(req.params.id);
148
+ if (!building) {
149
+ res.status(404).json({ error: `Building not found: ${req.params.id}` });
150
+ return;
151
+ }
152
+ res.json(redactBuilding(building));
153
+ });
154
+ // POST /api/buildings — create
155
+ router.post('/', async (req, res) => {
156
+ try {
157
+ const result = await buildingService.createBuilding(req.body, emit);
158
+ if (!result.ok) {
159
+ res.status(result.status).json({ error: 'Validation failed', errors: result.errors });
160
+ return;
161
+ }
162
+ res.status(201).json(redactBuilding(result.building));
163
+ }
164
+ catch (err) {
165
+ log.error('Failed to create building:', err);
166
+ res.status(500).json({ error: err.message });
167
+ }
168
+ });
169
+ // PATCH /api/buildings/:id — partial update
170
+ router.patch('/:id', async (req, res) => {
171
+ try {
172
+ const result = await buildingService.updateBuilding(req.params.id, req.body, emit);
173
+ if (!result.ok) {
174
+ res.status(result.status).json({
175
+ error: result.status === 404 ? 'Not found' : 'Validation failed',
176
+ errors: result.errors,
177
+ });
178
+ return;
179
+ }
180
+ res.json(redactBuilding(result.building));
181
+ }
182
+ catch (err) {
183
+ log.error(`Failed to update building ${req.params.id}:`, err);
184
+ res.status(500).json({ error: err.message });
185
+ }
186
+ });
187
+ // DELETE /api/buildings/:id?cleanup=false — delete (optionally skip runtime cleanup)
188
+ router.delete('/:id', async (req, res) => {
189
+ try {
190
+ const cleanup = req.query.cleanup !== 'false';
191
+ const result = await buildingService.deleteBuilding(req.params.id, emit, { cleanup });
192
+ if (!result.ok) {
193
+ res.status(result.status).json({ error: result.error });
194
+ return;
195
+ }
196
+ res.json({ deleted: true });
197
+ }
198
+ catch (err) {
199
+ log.error(`Failed to delete building ${req.params.id}:`, err);
200
+ res.status(500).json({ error: err.message });
201
+ }
202
+ });
203
+ // ─── Controls ─────────────────────────────────────────────────────────────────
204
+ // POST /api/buildings/:id/command — body { command }
205
+ router.post('/:id/command', async (req, res) => {
206
+ const command = req.body?.command;
207
+ if (!command || !VALID_COMMANDS.has(command)) {
208
+ res.status(400).json({
209
+ error: `command must be one of: ${[...VALID_COMMANDS].join(', ')}`,
210
+ });
211
+ return;
212
+ }
213
+ try {
214
+ const result = await buildingService.executeCommand(req.params.id, command, emit);
215
+ if (!result.success && result.error?.includes('not found')) {
216
+ res.status(404).json({ error: result.error });
217
+ return;
218
+ }
219
+ res.json(result);
220
+ }
221
+ catch (err) {
222
+ log.error(`Command ${command} failed for ${req.params.id}:`, err);
223
+ res.status(500).json({ error: err.message });
224
+ }
225
+ });
226
+ // GET /api/buildings/:id/logs?lines=200&service=foo — snapshot logs
227
+ router.get('/:id/logs', async (req, res) => {
228
+ const lines = Number.parseInt(String(req.query.lines ?? '200'), 10);
229
+ const service = typeof req.query.service === 'string' ? req.query.service : undefined;
230
+ try {
231
+ const result = await buildingService.getBuildingLogs(req.params.id, Number.isFinite(lines) && lines > 0 ? Math.min(lines, 5000) : 200, service);
232
+ if (!result.ok) {
233
+ res.status(result.status).json({ error: result.error });
234
+ return;
235
+ }
236
+ res.json({ source: result.source, logs: result.logs, lines: result.logs.split('\n').length });
237
+ }
238
+ catch (err) {
239
+ log.error(`Logs fetch failed for ${req.params.id}:`, err);
240
+ res.status(500).json({ error: err.message });
241
+ }
242
+ });
243
+ // POST /api/buildings/:id/sync-status — force PM2/Docker status refresh
244
+ router.post('/:id/sync-status', async (req, res) => {
245
+ const building = buildingService.getBuilding(req.params.id);
246
+ if (!building) {
247
+ res.status(404).json({ error: `Building not found: ${req.params.id}` });
248
+ return;
249
+ }
250
+ try {
251
+ if (building.pm2?.enabled) {
252
+ await buildingService.syncPM2Status(building.id, emit);
253
+ const refreshed = buildingService.getBuilding(building.id);
254
+ res.json({ source: 'pm2', status: refreshed?.status, pm2Status: refreshed?.pm2Status });
255
+ return;
256
+ }
257
+ if (building.docker?.enabled) {
258
+ await buildingService.syncDockerStatus(building.id, emit);
259
+ const refreshed = buildingService.getBuilding(building.id);
260
+ res.json({ source: 'docker', status: refreshed?.status, dockerStatus: refreshed?.dockerStatus });
261
+ return;
262
+ }
263
+ if (building.terminal?.enabled) {
264
+ const status = terminalService.getTerminalStatus(building);
265
+ res.json({ source: 'terminal', status: status ? 'running' : 'stopped', terminalStatus: status });
266
+ return;
267
+ }
268
+ res.json({ source: 'none', status: building.status });
269
+ }
270
+ catch (err) {
271
+ log.error(`sync-status failed for ${req.params.id}:`, err);
272
+ res.status(500).json({ error: err.message });
273
+ }
274
+ });
275
+ // POST /api/buildings/:id/subordinates — body { subordinateBuildingIds: string[] }
276
+ router.post('/:id/subordinates', async (req, res) => {
277
+ const ids = req.body?.subordinateBuildingIds;
278
+ if (!Array.isArray(ids) || ids.some((s) => typeof s !== 'string')) {
279
+ res.status(400).json({ error: 'subordinateBuildingIds must be an array of strings' });
280
+ return;
281
+ }
282
+ try {
283
+ const result = await buildingService.assignSubordinates(req.params.id, ids, emit);
284
+ if (!result.ok) {
285
+ res.status(result.status).json({
286
+ error: result.status === 404 ? 'Not found' : 'Invalid request',
287
+ errors: result.errors,
288
+ });
289
+ return;
290
+ }
291
+ res.json(redactBuilding(result.building));
292
+ }
293
+ catch (err) {
294
+ log.error(`assignSubordinates failed for ${req.params.id}:`, err);
295
+ res.status(500).json({ error: err.message });
296
+ }
297
+ });
298
+ export default router;
@@ -24,6 +24,7 @@ import eventRouter from './event-routes.js';
24
24
  import workflowRouter from './workflow-routes.js';
25
25
  import sessionsRouter from './sessions.js';
26
26
  import databaseRouter from './database.js';
27
+ import buildingsRouter, { setBroadcast as setBuildingsBroadcast } from './buildings.js';
27
28
  import { getPlugins } from '../integrations/integration-registry.js';
28
29
  const router = Router();
29
30
  // Health check
@@ -50,6 +51,7 @@ router.use('/events', eventRouter);
50
51
  router.use('/workflows', workflowRouter);
51
52
  router.use('/sessions', sessionsRouter);
52
53
  router.use('/database', databaseRouter);
54
+ router.use('/buildings', buildingsRouter);
53
55
  // Integration plugin routes (e.g. /api/slack/*, /api/documents/*, /api/jira/*)
54
56
  // Uses lazy lookup so plugins can be registered after route setup
55
57
  router.use((req, res, next) => {
@@ -68,5 +70,5 @@ router.use('/config', raw({ type: 'application/zip', limit: '100mb' }), configRo
68
70
  // Permission routes are mounted at root level since they're called as /api/permission-request
69
71
  router.use('/', permissionsRouter);
70
72
  // Export the broadcast setters for WebSocket handler to use
71
- export { setNotificationBroadcast, setExecBroadcast, setFocusAgentBroadcast, setAgentsBroadcast, setTriggerBroadcast };
73
+ export { setNotificationBroadcast, setExecBroadcast, setFocusAgentBroadcast, setAgentsBroadcast, setTriggerBroadcast, setBuildingsBroadcast };
72
74
  export default router;