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,338 @@
1
+ /**
2
+ * Provider Credentials Service (Grok + Codex)
3
+ *
4
+ * Same account-switcher model as claude-credentials-service, generalized for
5
+ * providers that keep a SINGLE active auth file plus named copies:
6
+ * - Grok: ~/.grok/auth.json + ~/.grok/auth.<name>.json
7
+ * - Codex: ~/.codex/auth.json + ~/.codex/auth.<name>.json
8
+ *
9
+ * Operators keep accounts as named profiles and promote one to active when the
10
+ * current account hits limits. Tokens are never returned to clients — only
11
+ * non-secret metadata (email, plan label, expiry) + a stable fingerprint.
12
+ */
13
+ import * as fs from 'fs';
14
+ import * as os from 'os';
15
+ import * as path from 'path';
16
+ import * as crypto from 'crypto';
17
+ import { createLogger } from '../utils/logger.js';
18
+ const log = createLogger('ProviderCredentials');
19
+ // --- JWT helper (metadata only; never logs or returns the token) -----------
20
+ function decodeJwtPayload(jwt) {
21
+ const parts = jwt.split('.');
22
+ if (parts.length < 2)
23
+ return null;
24
+ try {
25
+ const pad = parts[1] + '='.repeat((4 - (parts[1].length % 4)) % 4);
26
+ const json = Buffer.from(pad.replace(/-/g, '+').replace(/_/g, '/'), 'base64').toString('utf-8');
27
+ return JSON.parse(json);
28
+ }
29
+ catch {
30
+ return null;
31
+ }
32
+ }
33
+ function fingerprintToken(token) {
34
+ return crypto.createHash('sha256').update(token).digest('hex').slice(0, 12);
35
+ }
36
+ // --- Provider-specific parsers ---------------------------------------------
37
+ /** Grok ~/.grok/auth.json: dict keyed by "issuer::client_id" → { key, email, expires_at, ... }. */
38
+ function parseGrokCredentials(raw) {
39
+ try {
40
+ const data = JSON.parse(raw);
41
+ for (const entry of Object.values(data)) {
42
+ if (!entry || typeof entry !== 'object')
43
+ continue;
44
+ const token = typeof entry.key === 'string' ? entry.key : '';
45
+ if (!token)
46
+ continue;
47
+ const expiresRaw = entry.expires_at ?? entry.expiresAt;
48
+ let expiresAt = null;
49
+ if (typeof expiresRaw === 'number')
50
+ expiresAt = expiresRaw;
51
+ else if (typeof expiresRaw === 'string') {
52
+ const ms = Date.parse(expiresRaw);
53
+ expiresAt = Number.isFinite(ms) ? ms : null;
54
+ }
55
+ const email = typeof entry.email === 'string' ? entry.email : null;
56
+ const label = typeof entry.principal_type === 'string' ? entry.principal_type : null;
57
+ return { fingerprint: fingerprintToken(token), label, email, expiresAt, valid: true };
58
+ }
59
+ return { fingerprint: null, label: null, email: null, expiresAt: null, valid: false };
60
+ }
61
+ catch {
62
+ return { fingerprint: null, label: null, email: null, expiresAt: null, valid: false };
63
+ }
64
+ }
65
+ /** Codex ~/.codex/auth.json: { tokens: { access_token, id_token, ... } }; plan/email live in the id_token JWT. */
66
+ function parseCodexCredentials(raw) {
67
+ try {
68
+ const data = JSON.parse(raw);
69
+ const tokens = data.tokens;
70
+ const accessToken = typeof tokens?.access_token === 'string' ? tokens.access_token : '';
71
+ const idToken = typeof tokens?.id_token === 'string' ? tokens.id_token : '';
72
+ // API-key-only auth (no ChatGPT login) is also valid.
73
+ const apiKey = typeof data.OPENAI_API_KEY === 'string' ? data.OPENAI_API_KEY : '';
74
+ const fpToken = accessToken || apiKey;
75
+ if (!fpToken)
76
+ return { fingerprint: null, label: null, email: null, expiresAt: null, valid: false };
77
+ let email = null;
78
+ let label = apiKey && !accessToken ? 'API key' : null;
79
+ let expiresAt = null;
80
+ const claims = idToken ? decodeJwtPayload(idToken) : null;
81
+ if (claims) {
82
+ if (typeof claims.email === 'string')
83
+ email = claims.email;
84
+ const auth = claims['https://api.openai.com/auth'];
85
+ if (auth && typeof auth.chatgpt_plan_type === 'string')
86
+ label = auth.chatgpt_plan_type;
87
+ }
88
+ // Access-token expiry (its own JWT exp) — the value the CLI refreshes on.
89
+ const accessClaims = accessToken ? decodeJwtPayload(accessToken) : null;
90
+ if (accessClaims && typeof accessClaims.exp === 'number')
91
+ expiresAt = accessClaims.exp * 1000;
92
+ return { fingerprint: fingerprintToken(fpToken), label, email, expiresAt, valid: true };
93
+ }
94
+ catch {
95
+ return { fingerprint: null, label: null, email: null, expiresAt: null, valid: false };
96
+ }
97
+ }
98
+ // --- Provider registry (dir overridable for tests) -------------------------
99
+ let dirOverrides = {};
100
+ export function setProviderCredentialsDirForTests(overrides) {
101
+ dirOverrides = overrides;
102
+ }
103
+ function providerConfig(provider) {
104
+ if (provider === 'grok') {
105
+ return {
106
+ provider,
107
+ dir: dirOverrides.grok ?? path.join(os.homedir(), '.grok'),
108
+ activeFile: 'auth.json',
109
+ namedPrefix: 'auth.',
110
+ namedSuffix: '.json',
111
+ parse: parseGrokCredentials,
112
+ };
113
+ }
114
+ return {
115
+ provider,
116
+ dir: dirOverrides.codex ?? path.join(os.homedir(), '.codex'),
117
+ activeFile: 'auth.json',
118
+ namedPrefix: 'auth.',
119
+ namedSuffix: '.json',
120
+ parse: parseCodexCredentials,
121
+ };
122
+ }
123
+ const NAME_RE = /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,63}$/;
124
+ export function isValidProviderProfileName(name) {
125
+ if (typeof name !== 'string' || !NAME_RE.test(name))
126
+ return false;
127
+ if (name === 'json' || name.includes('/') || name.includes('\\') || name.includes('..'))
128
+ return false;
129
+ return true;
130
+ }
131
+ function assertValidName(name) {
132
+ if (!isValidProviderProfileName(name)) {
133
+ throw new Error(`Invalid profile name "${name}". Use 1–64 chars: letters, digits, dot, underscore, hyphen; must start with alphanumeric.`);
134
+ }
135
+ }
136
+ function activePath(cfg) {
137
+ return path.join(cfg.dir, cfg.activeFile);
138
+ }
139
+ function namedPath(cfg, name) {
140
+ return path.join(cfg.dir, `${cfg.namedPrefix}${name}${cfg.namedSuffix}`);
141
+ }
142
+ function parseFile(cfg, filePath) {
143
+ if (!fs.existsSync(filePath))
144
+ return null;
145
+ try {
146
+ const raw = fs.readFileSync(filePath, 'utf-8');
147
+ const meta = cfg.parse(raw);
148
+ return { raw, ...meta };
149
+ }
150
+ catch (err) {
151
+ log.warn(`Failed to parse ${cfg.provider} credentials at ${filePath}: ${err}`);
152
+ return { raw: '', fingerprint: null, label: null, email: null, expiresAt: null, valid: false };
153
+ }
154
+ }
155
+ function fileMtime(filePath) {
156
+ try {
157
+ return fs.statSync(filePath).mtimeMs;
158
+ }
159
+ catch {
160
+ return null;
161
+ }
162
+ }
163
+ function toMeta(cfg, id, name, filePath, parsed, isActive, matchesNamed) {
164
+ return {
165
+ id,
166
+ name,
167
+ isActive,
168
+ path: filePath,
169
+ valid: parsed?.valid ?? false,
170
+ fingerprint: parsed?.fingerprint ?? null,
171
+ label: parsed?.label ?? null,
172
+ email: parsed?.email ?? null,
173
+ expiresAt: parsed?.expiresAt ?? null,
174
+ mtimeMs: fileMtime(filePath),
175
+ matchesNamed,
176
+ };
177
+ }
178
+ function listNamedProfileNames(cfg) {
179
+ if (!fs.existsSync(cfg.dir))
180
+ return [];
181
+ const names = [];
182
+ for (const entry of fs.readdirSync(cfg.dir)) {
183
+ if (entry === cfg.activeFile)
184
+ continue;
185
+ if (!entry.startsWith(cfg.namedPrefix) || !entry.endsWith(cfg.namedSuffix))
186
+ continue;
187
+ const name = entry.slice(cfg.namedPrefix.length, entry.length - cfg.namedSuffix.length);
188
+ if (!name || !isValidProviderProfileName(name))
189
+ continue;
190
+ names.push(name);
191
+ }
192
+ names.sort((a, b) => a.localeCompare(b));
193
+ return names;
194
+ }
195
+ /** List the active auth file plus every named profile. Never returns tokens. */
196
+ export function listProviderCredentialProfiles(provider) {
197
+ const cfg = providerConfig(provider);
198
+ const activeFile = activePath(cfg);
199
+ const activeParsed = parseFile(cfg, activeFile);
200
+ const namedNames = listNamedProfileNames(cfg);
201
+ const namedByFp = new Map();
202
+ for (const name of namedNames) {
203
+ const parsed = parseFile(cfg, namedPath(cfg, name));
204
+ if (parsed?.fingerprint)
205
+ namedByFp.set(parsed.fingerprint, name);
206
+ }
207
+ const activeMatchesNamed = activeParsed?.fingerprint ? (namedByFp.get(activeParsed.fingerprint) ?? null) : null;
208
+ const active = fs.existsSync(activeFile)
209
+ ? toMeta(cfg, 'active', 'active', activeFile, activeParsed, true, activeMatchesNamed)
210
+ : null;
211
+ const profiles = namedNames.map((name) => {
212
+ const p = namedPath(cfg, name);
213
+ const parsed = parseFile(cfg, p);
214
+ const isActive = Boolean(activeParsed?.fingerprint) && parsed?.fingerprint === activeParsed?.fingerprint;
215
+ return toMeta(cfg, name, name, p, parsed, isActive, null);
216
+ });
217
+ return { provider, dir: cfg.dir, active, profiles };
218
+ }
219
+ function ensureDir(cfg) {
220
+ if (!fs.existsSync(cfg.dir))
221
+ fs.mkdirSync(cfg.dir, { recursive: true, mode: 0o700 });
222
+ }
223
+ /** Atomic write with restrictive permissions (credentials are secrets). */
224
+ function writeAtomic(cfg, filePath, content) {
225
+ ensureDir(cfg);
226
+ const tmp = path.join(cfg.dir, `.auth.tmp.${process.pid}.${Date.now()}`);
227
+ fs.writeFileSync(tmp, content, { encoding: 'utf-8', mode: 0o600 });
228
+ fs.renameSync(tmp, filePath);
229
+ try {
230
+ fs.chmodSync(filePath, 0o600);
231
+ }
232
+ catch {
233
+ // best-effort
234
+ }
235
+ }
236
+ /**
237
+ * Switch the active account to a named profile. Optionally stash the current
238
+ * active account under a name first (so an unsaved live login isn't lost).
239
+ */
240
+ export function switchProviderCredentialProfile(provider, name, opts = {}) {
241
+ assertValidName(name);
242
+ const cfg = providerConfig(provider);
243
+ const activeFile = activePath(cfg);
244
+ const targetFile = namedPath(cfg, name);
245
+ if (!fs.existsSync(targetFile))
246
+ throw new Error(`Named profile "${name}" not found`);
247
+ const targetParsed = parseFile(cfg, targetFile);
248
+ if (!targetParsed?.valid || !targetParsed.raw)
249
+ throw new Error(`Profile "${name}" has no valid credentials`);
250
+ const listing = listProviderCredentialProfiles(provider);
251
+ const previousMatchesNamed = listing.active?.matchesNamed ?? null;
252
+ let stashedAs = null;
253
+ if (listing.active?.valid) {
254
+ const stash = opts.stashActiveAs?.trim();
255
+ if (!previousMatchesNamed && stash) {
256
+ assertValidName(stash);
257
+ if (stash === name)
258
+ throw new Error('stashActiveAs cannot be the same as the profile you are switching to');
259
+ const stashFile = namedPath(cfg, stash);
260
+ if (fs.existsSync(stashFile)) {
261
+ const existing = parseFile(cfg, stashFile);
262
+ if (existing?.fingerprint && existing.fingerprint !== listing.active.fingerprint) {
263
+ throw new Error(`Profile "${stash}" already exists with different credentials. Choose another name.`);
264
+ }
265
+ }
266
+ const activeRaw = parseFile(cfg, activeFile)?.raw;
267
+ if (!activeRaw)
268
+ throw new Error('Failed to read active credentials for stash');
269
+ writeAtomic(cfg, stashFile, activeRaw);
270
+ stashedAs = stash;
271
+ log.log(`Stashed previous active ${provider} credentials as profile "${stash}"`);
272
+ }
273
+ else if (previousMatchesNamed) {
274
+ // Keep the matched named copy in sync with the (possibly refreshed) active file.
275
+ const matchedRaw = parseFile(cfg, activeFile)?.raw;
276
+ if (matchedRaw)
277
+ writeAtomic(cfg, namedPath(cfg, previousMatchesNamed), matchedRaw);
278
+ }
279
+ }
280
+ writeAtomic(cfg, activeFile, targetParsed.raw);
281
+ log.log(`Switched active ${provider} credentials to profile "${name}"`);
282
+ const after = listProviderCredentialProfiles(provider);
283
+ if (!after.active)
284
+ throw new Error('Active credentials missing after switch');
285
+ return { ok: true, active: after.active, profiles: after.profiles, stashedAs, previousMatchesNamed };
286
+ }
287
+ /** Save (copy) the current active credentials as a named profile. */
288
+ export function saveActiveProviderCredentialProfile(provider, name, opts = {}) {
289
+ assertValidName(name);
290
+ const cfg = providerConfig(provider);
291
+ const activeParsed = parseFile(cfg, activePath(cfg));
292
+ if (!activeParsed?.valid || !activeParsed.raw)
293
+ throw new Error(`No valid active ${provider} credentials to save`);
294
+ const dest = namedPath(cfg, name);
295
+ if (fs.existsSync(dest) && !opts.force) {
296
+ const existing = parseFile(cfg, dest);
297
+ if (existing?.fingerprint && existing.fingerprint !== activeParsed.fingerprint) {
298
+ throw new Error(`Profile "${name}" already exists with different credentials. Pass force=true to overwrite.`);
299
+ }
300
+ }
301
+ writeAtomic(cfg, dest, activeParsed.raw);
302
+ log.log(`Saved active ${provider} credentials as profile "${name}"`);
303
+ return listProviderCredentialProfiles(provider);
304
+ }
305
+ /** Rename a named profile file. */
306
+ export function renameProviderCredentialProfile(provider, from, to) {
307
+ assertValidName(from);
308
+ assertValidName(to);
309
+ if (from === to)
310
+ return listProviderCredentialProfiles(provider);
311
+ const cfg = providerConfig(provider);
312
+ const src = namedPath(cfg, from);
313
+ const dest = namedPath(cfg, to);
314
+ if (!fs.existsSync(src))
315
+ throw new Error(`Named profile "${from}" not found`);
316
+ if (fs.existsSync(dest))
317
+ throw new Error(`Profile "${to}" already exists`);
318
+ fs.renameSync(src, dest);
319
+ try {
320
+ fs.chmodSync(dest, 0o600);
321
+ }
322
+ catch {
323
+ // ignore
324
+ }
325
+ log.log(`Renamed ${provider} credentials profile "${from}" → "${to}"`);
326
+ return listProviderCredentialProfiles(provider);
327
+ }
328
+ /** Delete a named profile. */
329
+ export function deleteProviderCredentialProfile(provider, name) {
330
+ assertValidName(name);
331
+ const cfg = providerConfig(provider);
332
+ const file = namedPath(cfg, name);
333
+ if (!fs.existsSync(file))
334
+ throw new Error(`Named profile "${name}" not found`);
335
+ fs.unlinkSync(file);
336
+ log.log(`Deleted ${provider} credentials profile "${name}"`);
337
+ return listProviderCredentialProfiles(provider);
338
+ }
@@ -293,3 +293,97 @@ export function setInteractiveModeEnabled(enabled) {
293
293
  throw error;
294
294
  }
295
295
  }
296
+ // ============================================================================
297
+ // Codex App-Server Mode Setting
298
+ // ============================================================================
299
+ //
300
+ // Experimental: when enabled, Codex agents run through a persistent
301
+ // `codex app-server` JSON-RPC process instead of one `codex exec` process per
302
+ // turn. The app-server protocol streams word-by-word agent-message deltas
303
+ // (item/agentMessage/delta), which `codex exec --experimental-json` never
304
+ // emits, so this is the only way Codex replies can typewriter in the UI.
305
+ // Like interactive mode, the setting is read live at each launch — no server
306
+ // restart needed to switch.
307
+ const CODEX_APP_SERVER_MODE_FILE = path.join(DATA_DIR, 'codex-app-server-mode.json');
308
+ /**
309
+ * Check if experimental Codex app-server (streaming) mode is enabled.
310
+ */
311
+ export function isCodexAppServerModeEnabled() {
312
+ ensureDataDir();
313
+ try {
314
+ if (fs.existsSync(CODEX_APP_SERVER_MODE_FILE)) {
315
+ const data = JSON.parse(fs.readFileSync(CODEX_APP_SERVER_MODE_FILE, 'utf-8'));
316
+ return data.enabled;
317
+ }
318
+ }
319
+ catch (error) {
320
+ log.error(` Failed to load codex app-server mode setting: ${error.message}`);
321
+ }
322
+ // Default ON: streaming (app-server) is the default for Codex agents. An
323
+ // explicit toggle-off is still respected (the file above wins).
324
+ return true;
325
+ }
326
+ /**
327
+ * Enable/disable experimental Codex app-server (streaming) mode.
328
+ */
329
+ export function setCodexAppServerModeEnabled(enabled) {
330
+ ensureDataDir();
331
+ const data = {
332
+ enabled,
333
+ updatedAt: Date.now(),
334
+ };
335
+ try {
336
+ fs.writeFileSync(CODEX_APP_SERVER_MODE_FILE, JSON.stringify(data, null, 2), 'utf-8');
337
+ log.log(` Codex app-server mode setting updated: enabled=${enabled}`);
338
+ }
339
+ catch (error) {
340
+ log.error(` Failed to save codex app-server mode setting: ${error.message}`);
341
+ throw error;
342
+ }
343
+ }
344
+ // ============================================================================
345
+ // OpenCode Server Mode Setting
346
+ // ============================================================================
347
+ //
348
+ // Experimental: when enabled, OpenCode agents run through a persistent, detached
349
+ // `opencode serve` HTTP server instead of one `opencode run` process per turn.
350
+ // The server streams word-by-word `message.part.delta` tokens over SSE (which
351
+ // `opencode run --format json` never emits) and survives commander restarts.
352
+ // Read live at each launch — no server restart needed to switch.
353
+ const OPENCODE_SERVER_MODE_FILE = path.join(DATA_DIR, 'opencode-server-mode.json');
354
+ /**
355
+ * Check if experimental OpenCode server (streaming) mode is enabled.
356
+ */
357
+ export function isOpencodeServerModeEnabled() {
358
+ ensureDataDir();
359
+ try {
360
+ if (fs.existsSync(OPENCODE_SERVER_MODE_FILE)) {
361
+ const data = JSON.parse(fs.readFileSync(OPENCODE_SERVER_MODE_FILE, 'utf-8'));
362
+ return data.enabled;
363
+ }
364
+ }
365
+ catch (error) {
366
+ log.error(` Failed to load opencode server mode setting: ${error.message}`);
367
+ }
368
+ // Default ON: streaming (opencode serve) is the default for OpenCode agents.
369
+ // An explicit toggle-off is still respected (the file above wins).
370
+ return true;
371
+ }
372
+ /**
373
+ * Enable/disable experimental OpenCode server (streaming) mode.
374
+ */
375
+ export function setOpencodeServerModeEnabled(enabled) {
376
+ ensureDataDir();
377
+ const data = {
378
+ enabled,
379
+ updatedAt: Date.now(),
380
+ };
381
+ try {
382
+ fs.writeFileSync(OPENCODE_SERVER_MODE_FILE, JSON.stringify(data, null, 2), 'utf-8');
383
+ log.log(` OpenCode server mode setting updated: enabled=${enabled}`);
384
+ }
385
+ catch (error) {
386
+ log.error(` Failed to save opencode server mode setting: ${error.message}`);
387
+ throw error;
388
+ }
389
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tide-commander",
3
- "version": "1.153.0",
3
+ "version": "1.155.0",
4
4
  "description": "Visual multi-agent orchestrator and manager for Claude Code with 3D/2D interface",
5
5
  "repository": {
6
6
  "type": "git",