tide-commander 1.153.0 → 1.155.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 (77) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/dist/assets/{BossLogsModal-Dgz-N5gw.js → BossLogsModal-OFobifVq.js} +1 -1
  3. package/dist/assets/{BossSpawnModal-BK_rYu8s.js → BossSpawnModal-Dho-x6rm.js} +1 -1
  4. package/dist/assets/{ControlsModal-hC_AiCpk.js → ControlsModal-DJBDhvNO.js} +1 -1
  5. package/dist/assets/{DockerLogsModal-D7ogyaZr.js → DockerLogsModal-C9TRTcsO.js} +1 -1
  6. package/dist/assets/{EmbeddedEditor-BFzUOmwz.js → EmbeddedEditor-GDE_Khwz.js} +1 -1
  7. package/dist/assets/{GmailOAuthSetup-C_Yc5Ypv.js → GmailOAuthSetup-D9m5zMWC.js} +1 -1
  8. package/dist/assets/{GoogleOAuthSetup-CefQSOYm.js → GoogleOAuthSetup-BxkN7LjY.js} +1 -1
  9. package/dist/assets/{IframeModal-BbeuiE04.js → IframeModal-Do_I_wI7.js} +1 -1
  10. package/dist/assets/{IntegrationsPanel-xh3TgJLH.js → IntegrationsPanel-jRrwC2-G.js} +2 -2
  11. package/dist/assets/{LogViewerModal-CE2L8nWJ.js → LogViewerModal-BtcUe5i2.js} +1 -1
  12. package/dist/assets/{MonitoringModal-A2SN3uAJ.js → MonitoringModal-BTmSXrzv.js} +1 -1
  13. package/dist/assets/{PM2LogsModal-DdlhCa8u.js → PM2LogsModal-CUf7NGHz.js} +1 -1
  14. package/dist/assets/{RestoreArchivedAreaModal-BvHo-OxE.js → RestoreArchivedAreaModal-B72UKeRP.js} +1 -1
  15. package/dist/assets/{Scene2DCanvas-t6RuUegr.js → Scene2DCanvas-BCfxX7JU.js} +1 -1
  16. package/dist/assets/{SceneManager-TQ2_AvFn.js → SceneManager-BQubdlgd.js} +4 -4
  17. package/dist/assets/{SkillsPanel-PjzjcaoG.js → SkillsPanel-3qreMiJs.js} +1 -1
  18. package/dist/assets/{SlackMultiInstanceSetup-ms4r32Z8.js → SlackMultiInstanceSetup-BSCT5nAB.js} +1 -1
  19. package/dist/assets/{SpawnModal-B1FS8m9h.js → SpawnModal-DaWjnugM.js} +1 -1
  20. package/dist/assets/{StatisticsModal-mmz8hnD5.js → StatisticsModal-DCjCAU2o.js} +1 -1
  21. package/dist/assets/{SubordinateAssignmentModal-BXoG-Xgd.js → SubordinateAssignmentModal-9iPXraIi.js} +1 -1
  22. package/dist/assets/{TriggerManagerPanel-ODJyjvY-.js → TriggerManagerPanel-B4dCLEi_.js} +1 -1
  23. package/dist/assets/{WorkflowEditorPanel-9wtY0Tn3.js → WorkflowEditorPanel-BaYXKfYH.js} +1 -1
  24. package/dist/assets/{area-logos-BLyzmh47.js → area-logos-CgVT13bp.js} +1 -1
  25. package/dist/assets/{index-CN2p2fxH.js → index-0y0WY2XK.js} +1 -1
  26. package/dist/assets/index-BPjQZacc.css +1 -0
  27. package/dist/assets/{index-CdbLxNJr.js → index-C-tbJxGb.js} +1 -1
  28. package/dist/assets/{index-1hwiSf7o.js → index-C3Yb-3TP.js} +1 -1
  29. package/dist/assets/index-C3jAbuv2.js +1 -0
  30. package/dist/assets/{index-Ci2ZKu0n.js → index-C7t_6aP3.js} +2 -2
  31. package/dist/assets/{index-CBkh8xcE.js → index-CetQ5OX1.js} +1 -1
  32. package/dist/assets/{index-Cf-PINah.js → index-D8dlRudN.js} +3 -3
  33. package/dist/assets/index-DNjdlOwh.js +1 -0
  34. package/dist/assets/{index-D8vFfLs4.js → index-DiZy3nuB.js} +1 -1
  35. package/dist/assets/main-BHBs2E1L.css +1 -0
  36. package/dist/assets/{main-9TcmAtcq.js → main-DDwa0x9e.js} +113 -113
  37. package/dist/assets/{web-BVIPIXA1.js → web-Dj2DYQG1.js} +1 -1
  38. package/dist/assets/{web-Cz3TYK-R.js → web-EBdZk56y.js} +1 -1
  39. package/dist/assets/{web-BAkfcL4w.js → web-PXjd4vZ8.js} +1 -1
  40. package/dist/build-info.json +1 -1
  41. package/dist/index.html +2 -2
  42. package/dist/locales/de/config.json +1 -0
  43. package/dist/locales/en/config.json +6 -1
  44. package/dist/locales/en/terminal.json +7 -1
  45. package/dist/locales/es/config.json +4 -1
  46. package/dist/locales/es/terminal.json +8 -0
  47. package/dist/locales/fr/config.json +1 -0
  48. package/dist/locales/hi/config.json +1 -0
  49. package/dist/locales/it/config.json +1 -0
  50. package/dist/locales/ja/config.json +1 -0
  51. package/dist/locales/pt/config.json +1 -0
  52. package/dist/locales/ru/config.json +1 -0
  53. package/dist/locales/zh-CN/config.json +1 -0
  54. package/dist/src/packages/server/codex/app-server/app-server-event-adapter.js +222 -0
  55. package/dist/src/packages/server/codex/app-server/app-server-process.js +304 -0
  56. package/dist/src/packages/server/codex/app-server/app-server-recovery-store.js +76 -0
  57. package/dist/src/packages/server/codex/app-server/codex-app-server-runner.js +453 -0
  58. package/dist/src/packages/server/opencode/json-event-parser.js +3 -1
  59. package/dist/src/packages/server/opencode/server/opencode-server-event-adapter.js +127 -0
  60. package/dist/src/packages/server/opencode/server/opencode-server-process.js +285 -0
  61. package/dist/src/packages/server/opencode/server/opencode-server-recovery-store.js +73 -0
  62. package/dist/src/packages/server/opencode/server/opencode-server-runner.js +384 -0
  63. package/dist/src/packages/server/routes/agents.js +57 -1
  64. package/dist/src/packages/server/routes/system.js +110 -1
  65. package/dist/src/packages/server/runtime/codex-runner-router.js +99 -0
  66. package/dist/src/packages/server/runtime/codex-runtime-provider.js +5 -3
  67. package/dist/src/packages/server/runtime/opencode-runner-router.js +93 -0
  68. package/dist/src/packages/server/runtime/opencode-runtime-provider.js +5 -3
  69. package/dist/src/packages/server/services/claude-credentials-service.js +208 -1
  70. package/dist/src/packages/server/services/claude-usage-service.js +35 -28
  71. package/dist/src/packages/server/services/provider-credentials-service.js +338 -0
  72. package/dist/src/packages/server/services/system-prompt-service.js +94 -0
  73. package/package.json +1 -1
  74. package/dist/assets/index-CbfoJ_Vc.js +0 -1
  75. package/dist/assets/index-E5IMeM80.js +0 -1
  76. package/dist/assets/index-XI0_ICy6.css +0 -1
  77. package/dist/assets/main-DP0iv8Le.css +0 -1
@@ -0,0 +1,285 @@
1
+ /**
2
+ * OpencodeServerProcess
3
+ *
4
+ * Manages a DETACHED, restart-surviving `opencode serve` HTTP server and speaks
5
+ * its API: one SSE stream (`GET /event`) carries every session's events
6
+ * (word-by-word `message.part.delta`), and prompts/sessions/aborts go over plain
7
+ * HTTP. One server multiplexes every OpenCode agent — each agent owns a
8
+ * `sessionID`, and every event carries `properties.sessionID` so the runner can
9
+ * fan them back out.
10
+ *
11
+ * Why detached + HTTP/SSE (vs `opencode run` per turn): `opencode run --format
12
+ * json` emits one full assistant message (no token deltas), and a child process
13
+ * dies with the commander. A detached server bound to a localhost port survives
14
+ * commander restarts and keeps running any in-flight turn; on restart the runner
15
+ * reconnects the SSE stream and OpenCode's persisted sessions resume seamlessly.
16
+ */
17
+ import { spawn } from 'child_process';
18
+ import * as http from 'http';
19
+ import * as net from 'net';
20
+ import * as fs from 'fs';
21
+ import { createLogger } from '../../utils/logger.js';
22
+ import { loadDaemonInfo, saveDaemonInfo, clearDaemonInfo, OPENCODE_SERVER_LOG_FILE, } from './opencode-server-recovery-store.js';
23
+ const log = createLogger('OpencodeServer');
24
+ const HEALTH_TIMEOUT_MS = 25000;
25
+ const HEALTH_POLL_MS = 300;
26
+ const SSE_RECONNECT_MS = 1000;
27
+ export class OpencodeServerProcess {
28
+ opts;
29
+ port = null;
30
+ sseReq = null;
31
+ stopped = false;
32
+ connected = false;
33
+ reconnectedToExisting = false;
34
+ constructor(opts) {
35
+ this.opts = opts;
36
+ }
37
+ isAlive() {
38
+ return this.connected && !this.stopped;
39
+ }
40
+ didReconnect() {
41
+ return this.reconnectedToExisting;
42
+ }
43
+ get baseUrl() {
44
+ return `http://127.0.0.1:${this.port}`;
45
+ }
46
+ async ensureStarted() {
47
+ if (this.connected)
48
+ return;
49
+ this.stopped = false;
50
+ // 1. Rejoin an already-running daemon if healthy.
51
+ const existing = loadDaemonInfo();
52
+ if (existing && isPidAlive(existing.pid) && (await probeHealth(existing.port, 1500))) {
53
+ log.log(`🔗 Rejoining existing opencode server pid=${existing.pid} port=${existing.port}`);
54
+ this.port = existing.port;
55
+ this.reconnectedToExisting = true;
56
+ this.openSse();
57
+ this.connected = true;
58
+ return;
59
+ }
60
+ // 2. Spawn a fresh detached daemon.
61
+ this.reconnectedToExisting = false;
62
+ const port = await findFreePort();
63
+ const pid = this.spawnDaemon(port);
64
+ saveDaemonInfo({ port, pid, startedAt: Date.now() });
65
+ this.port = port;
66
+ await this.waitForHealth(port);
67
+ this.openSse();
68
+ this.connected = true;
69
+ }
70
+ spawnDaemon(port) {
71
+ const { executablePath, cwd, extraEnv } = this.opts;
72
+ const logFd = (() => {
73
+ try {
74
+ return fs.openSync(OPENCODE_SERVER_LOG_FILE, 'a');
75
+ }
76
+ catch {
77
+ return 'ignore';
78
+ }
79
+ })();
80
+ log.log(`🚀 Spawning detached opencode server: ${executablePath} serve --port ${port}`);
81
+ const child = spawn(executablePath, ['serve', '--port', String(port), '--hostname', '127.0.0.1'], {
82
+ cwd,
83
+ env: { ...process.env, ...extraEnv },
84
+ detached: true,
85
+ stdio: ['ignore', logFd, logFd],
86
+ });
87
+ child.unref();
88
+ child.on('error', (err) => log.error(`opencode serve spawn error: ${err.message}`));
89
+ return child.pid ?? -1;
90
+ }
91
+ async waitForHealth(port) {
92
+ const deadline = Date.now() + HEALTH_TIMEOUT_MS;
93
+ while (Date.now() < deadline) {
94
+ if (await probeHealth(port, 1000))
95
+ return;
96
+ await sleep(HEALTH_POLL_MS);
97
+ }
98
+ throw new Error('opencode server /api/health never became healthy');
99
+ }
100
+ /** Open the single global SSE stream and parse `data:` frames. */
101
+ openSse() {
102
+ if (this.stopped)
103
+ return;
104
+ const req = http.get(`${this.baseUrl}/event`, { headers: { Accept: 'text/event-stream' } }, (res) => {
105
+ res.setEncoding('utf8');
106
+ let buffer = '';
107
+ res.on('data', (chunk) => {
108
+ buffer += chunk;
109
+ // SSE frames are separated by a blank line.
110
+ let idx;
111
+ while ((idx = buffer.indexOf('\n\n')) >= 0) {
112
+ const frame = buffer.slice(0, idx);
113
+ buffer = buffer.slice(idx + 2);
114
+ this.handleFrame(frame);
115
+ }
116
+ });
117
+ res.on('end', () => this.handleSseClosed());
118
+ });
119
+ this.sseReq = req;
120
+ req.on('error', (err) => {
121
+ log.warn(`SSE connection error: ${err.message}`);
122
+ this.handleSseClosed();
123
+ });
124
+ }
125
+ handleFrame(frame) {
126
+ // A frame is one or more lines; take the `data:` payload(s).
127
+ const dataLines = frame
128
+ .split('\n')
129
+ .filter((l) => l.startsWith('data:'))
130
+ .map((l) => l.slice(5).trim());
131
+ if (dataLines.length === 0)
132
+ return;
133
+ const payload = dataLines.join('\n');
134
+ if (!payload)
135
+ return;
136
+ let evt;
137
+ try {
138
+ evt = JSON.parse(payload);
139
+ }
140
+ catch {
141
+ return;
142
+ }
143
+ this.opts.onEvent(evt);
144
+ }
145
+ handleSseClosed() {
146
+ this.sseReq = null;
147
+ if (this.stopped)
148
+ return;
149
+ // Try to re-establish once; if the server is truly gone, give up and notify.
150
+ log.warn('SSE stream closed; attempting to reconnect');
151
+ setTimeout(async () => {
152
+ if (this.stopped || !this.port)
153
+ return;
154
+ if (await probeHealth(this.port, 1500)) {
155
+ this.openSse();
156
+ }
157
+ else {
158
+ this.connected = false;
159
+ this.opts.onDisconnect();
160
+ }
161
+ }, SSE_RECONNECT_MS);
162
+ }
163
+ // ---- HTTP helpers --------------------------------------------------------
164
+ async httpJson(method, path, body) {
165
+ const res = await fetch(`${this.baseUrl}${path}`, {
166
+ method,
167
+ headers: body !== undefined ? { 'Content-Type': 'application/json' } : undefined,
168
+ body: body !== undefined ? JSON.stringify(body) : undefined,
169
+ });
170
+ if (!res.ok) {
171
+ throw new Error(`${method} ${path} → ${res.status} ${res.statusText}`);
172
+ }
173
+ const text = await res.text();
174
+ return text ? JSON.parse(text) : null;
175
+ }
176
+ /** Create a session (optionally scoped to a working directory). */
177
+ async createSession(directory) {
178
+ const body = {};
179
+ if (directory)
180
+ body.location = { directory };
181
+ const resp = await this.httpJson('POST', '/api/session', body);
182
+ const id = resp?.data?.id ?? resp?.id;
183
+ if (!id)
184
+ throw new Error('opencode session create returned no id');
185
+ return id;
186
+ }
187
+ /** Send a prompt to a session. Fire-and-forget: streaming arrives via SSE. */
188
+ sendPrompt(sessionId, text, model) {
189
+ const body = { parts: [{ type: 'text', text }] };
190
+ if (model)
191
+ body.model = model;
192
+ return this.httpJson('POST', `/session/${sessionId}/message`, body).then(() => undefined);
193
+ }
194
+ async abort(sessionId) {
195
+ try {
196
+ await this.httpJson('POST', `/session/${sessionId}/abort`, {});
197
+ }
198
+ catch { /* best-effort */ }
199
+ }
200
+ /** Session IDs currently generating (busy), used to detect turns still in flight. */
201
+ async activeSessionIds() {
202
+ try {
203
+ const resp = await this.httpJson('GET', '/api/session/active');
204
+ const list = (resp?.data ?? resp);
205
+ const ids = new Set();
206
+ if (Array.isArray(list)) {
207
+ for (const item of list) {
208
+ if (typeof item === 'string')
209
+ ids.add(item);
210
+ else if (item?.id)
211
+ ids.add(item.id);
212
+ else if (item?.sessionID)
213
+ ids.add(item.sessionID);
214
+ }
215
+ }
216
+ return ids;
217
+ }
218
+ catch {
219
+ return new Set();
220
+ }
221
+ }
222
+ /** Disconnect our client but LEAVE the daemon running (survives restart). */
223
+ disconnect() {
224
+ this.stopped = true;
225
+ this.connected = false;
226
+ if (this.sseReq) {
227
+ try {
228
+ this.sseReq.destroy();
229
+ }
230
+ catch { /* ignore */ }
231
+ this.sseReq = null;
232
+ }
233
+ }
234
+ /** Disconnect AND kill the daemon (used on explicit full shutdown). */
235
+ killDaemon() {
236
+ this.disconnect();
237
+ const info = loadDaemonInfo();
238
+ if (info && isPidAlive(info.pid)) {
239
+ try {
240
+ process.kill(info.pid, 'SIGTERM');
241
+ }
242
+ catch { /* ignore */ }
243
+ }
244
+ clearDaemonInfo();
245
+ }
246
+ }
247
+ function isPidAlive(pid) {
248
+ if (!pid || pid < 1)
249
+ return false;
250
+ try {
251
+ process.kill(pid, 0);
252
+ return true;
253
+ }
254
+ catch {
255
+ return false;
256
+ }
257
+ }
258
+ function probeHealth(port, timeoutMs) {
259
+ return new Promise((resolve) => {
260
+ const req = http.get({ host: '127.0.0.1', port, path: '/api/health', timeout: timeoutMs }, (res) => {
261
+ res.resume();
262
+ resolve(res.statusCode === 200);
263
+ });
264
+ req.on('error', () => resolve(false));
265
+ req.on('timeout', () => {
266
+ req.destroy();
267
+ resolve(false);
268
+ });
269
+ });
270
+ }
271
+ function findFreePort() {
272
+ return new Promise((resolve, reject) => {
273
+ const srv = net.createServer();
274
+ srv.unref();
275
+ srv.on('error', reject);
276
+ srv.listen(0, '127.0.0.1', () => {
277
+ const addr = srv.address();
278
+ const port = typeof addr === 'object' && addr ? addr.port : 0;
279
+ srv.close(() => (port ? resolve(port) : reject(new Error('could not allocate a free port'))));
280
+ });
281
+ });
282
+ }
283
+ function sleep(ms) {
284
+ return new Promise((r) => setTimeout(r, ms));
285
+ }
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Persistence for the restart-surviving OpenCode `serve` daemon.
3
+ *
4
+ * Mirrors the Codex app-server recovery store. Persists, in a dedicated file:
5
+ * 1. Daemon info — the HTTP port + pid of the detached `opencode serve` process,
6
+ * so a restarted commander probes it and reconnects instead of respawning.
7
+ * 2. Per-agent session state — agentId ↔ OpenCode sessionID + turn state, so we
8
+ * can re-attach the SSE stream and finalize any turn that was mid-flight
9
+ * during the restart.
10
+ */
11
+ import * as fs from 'fs';
12
+ import * as path from 'path';
13
+ import * as os from 'os';
14
+ import { createLogger } from '../../utils/logger.js';
15
+ const log = createLogger('OpencodeServerStore');
16
+ const DATA_DIR = path.join(process.env.XDG_DATA_HOME || path.join(os.homedir(), '.local', 'share'), 'tide-commander');
17
+ const DAEMON_FILE = path.join(DATA_DIR, 'opencode-server-daemon.json');
18
+ const AGENTS_FILE = path.join(DATA_DIR, 'opencode-server-agents.json');
19
+ export const OPENCODE_SERVER_LOG_FILE = path.join(DATA_DIR, 'opencode-server.log');
20
+ function ensureDataDir() {
21
+ if (!fs.existsSync(DATA_DIR))
22
+ fs.mkdirSync(DATA_DIR, { recursive: true });
23
+ }
24
+ export function loadDaemonInfo() {
25
+ try {
26
+ if (!fs.existsSync(DAEMON_FILE))
27
+ return null;
28
+ const data = JSON.parse(fs.readFileSync(DAEMON_FILE, 'utf-8'));
29
+ if (typeof data.port === 'number' && typeof data.pid === 'number')
30
+ return data;
31
+ }
32
+ catch (err) {
33
+ log.warn(`Failed to load daemon info: ${err.message}`);
34
+ }
35
+ return null;
36
+ }
37
+ export function saveDaemonInfo(info) {
38
+ ensureDataDir();
39
+ try {
40
+ fs.writeFileSync(DAEMON_FILE, JSON.stringify(info, null, 2), 'utf-8');
41
+ }
42
+ catch (err) {
43
+ log.error(`Failed to save daemon info: ${err.message}`);
44
+ }
45
+ }
46
+ export function clearDaemonInfo() {
47
+ try {
48
+ if (fs.existsSync(DAEMON_FILE))
49
+ fs.unlinkSync(DAEMON_FILE);
50
+ }
51
+ catch { /* ignore */ }
52
+ }
53
+ export function loadOpencodeAgents() {
54
+ try {
55
+ if (!fs.existsSync(AGENTS_FILE))
56
+ return [];
57
+ const data = JSON.parse(fs.readFileSync(AGENTS_FILE, 'utf-8'));
58
+ return Array.isArray(data) ? data : [];
59
+ }
60
+ catch (err) {
61
+ log.warn(`Failed to load opencode agents: ${err.message}`);
62
+ return [];
63
+ }
64
+ }
65
+ export function saveOpencodeAgents(agents) {
66
+ ensureDataDir();
67
+ try {
68
+ fs.writeFileSync(AGENTS_FILE, JSON.stringify(agents, null, 2), 'utf-8');
69
+ }
70
+ catch (err) {
71
+ log.error(`Failed to save opencode agents: ${err.message}`);
72
+ }
73
+ }