@panorama-ai/gateway 2.24.100

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 (58) hide show
  1. package/README.md +74 -0
  2. package/dist/cli-providers/claude-utils.d.ts +10 -0
  3. package/dist/cli-providers/claude-utils.d.ts.map +1 -0
  4. package/dist/cli-providers/claude-utils.js +73 -0
  5. package/dist/cli-providers/claude-utils.js.map +1 -0
  6. package/dist/cli-providers/claude.d.ts +3 -0
  7. package/dist/cli-providers/claude.d.ts.map +1 -0
  8. package/dist/cli-providers/claude.js +212 -0
  9. package/dist/cli-providers/claude.js.map +1 -0
  10. package/dist/cli-providers/codex-schema.d.ts +10 -0
  11. package/dist/cli-providers/codex-schema.d.ts.map +1 -0
  12. package/dist/cli-providers/codex-schema.js +76 -0
  13. package/dist/cli-providers/codex-schema.js.map +1 -0
  14. package/dist/cli-providers/codex.d.ts +3 -0
  15. package/dist/cli-providers/codex.d.ts.map +1 -0
  16. package/dist/cli-providers/codex.js +271 -0
  17. package/dist/cli-providers/codex.js.map +1 -0
  18. package/dist/cli-providers/gemini.d.ts +3 -0
  19. package/dist/cli-providers/gemini.d.ts.map +1 -0
  20. package/dist/cli-providers/gemini.js +214 -0
  21. package/dist/cli-providers/gemini.js.map +1 -0
  22. package/dist/cli-providers/registry.d.ts +5 -0
  23. package/dist/cli-providers/registry.d.ts.map +1 -0
  24. package/dist/cli-providers/registry.js +25 -0
  25. package/dist/cli-providers/registry.js.map +1 -0
  26. package/dist/cli-providers/types.d.ts +61 -0
  27. package/dist/cli-providers/types.d.ts.map +1 -0
  28. package/dist/cli-providers/types.js +2 -0
  29. package/dist/cli-providers/types.js.map +1 -0
  30. package/dist/index.d.ts +2 -0
  31. package/dist/index.d.ts.map +1 -0
  32. package/dist/index.js +2288 -0
  33. package/dist/index.js.map +1 -0
  34. package/dist/subagent-adapters/claude-code.d.ts +3 -0
  35. package/dist/subagent-adapters/claude-code.d.ts.map +1 -0
  36. package/dist/subagent-adapters/claude-code.js +565 -0
  37. package/dist/subagent-adapters/claude-code.js.map +1 -0
  38. package/dist/subagent-adapters/claude-support.d.ts +30 -0
  39. package/dist/subagent-adapters/claude-support.d.ts.map +1 -0
  40. package/dist/subagent-adapters/claude-support.js +67 -0
  41. package/dist/subagent-adapters/claude-support.js.map +1 -0
  42. package/dist/subagent-adapters/codex.d.ts +3 -0
  43. package/dist/subagent-adapters/codex.d.ts.map +1 -0
  44. package/dist/subagent-adapters/codex.js +241 -0
  45. package/dist/subagent-adapters/codex.js.map +1 -0
  46. package/dist/subagent-adapters/gemini.d.ts +3 -0
  47. package/dist/subagent-adapters/gemini.d.ts.map +1 -0
  48. package/dist/subagent-adapters/gemini.js +257 -0
  49. package/dist/subagent-adapters/gemini.js.map +1 -0
  50. package/dist/subagent-adapters/registry.d.ts +4 -0
  51. package/dist/subagent-adapters/registry.d.ts.map +1 -0
  52. package/dist/subagent-adapters/registry.js +19 -0
  53. package/dist/subagent-adapters/registry.js.map +1 -0
  54. package/dist/subagent-adapters/types.d.ts +60 -0
  55. package/dist/subagent-adapters/types.d.ts.map +1 -0
  56. package/dist/subagent-adapters/types.js +2 -0
  57. package/dist/subagent-adapters/types.js.map +1 -0
  58. package/package.json +36 -0
package/dist/index.js ADDED
@@ -0,0 +1,2288 @@
1
+ import { createClient } from '@supabase/supabase-js';
2
+ import { execFile, spawn } from 'node:child_process';
3
+ import { randomUUID } from 'node:crypto';
4
+ import dotenv from 'dotenv';
5
+ import fs from 'node:fs/promises';
6
+ import fsSync from 'node:fs';
7
+ import os from 'node:os';
8
+ import path from 'node:path';
9
+ import process from 'node:process';
10
+ import { promisify } from 'node:util';
11
+ import { DEFAULT_CLAUDE_SUPPORT, coerceClaudeSupport, parseClaudeSupport, toFlagRecord, } from './subagent-adapters/claude-support.js';
12
+ import { getSubagentAdapter } from './subagent-adapters/registry.js';
13
+ import { SUBAGENT_SCHEMA_VERSION, } from './subagent-adapters/types.js';
14
+ import { listGatewayCliProviders, getGatewayCliProvider, inferGatewayProviderFromModel } from './cli-providers/registry.js';
15
+ const execFileAsync = promisify(execFile);
16
+ const DEFAULT_HEARTBEAT_INTERVAL_MS = 30_000;
17
+ const DEFAULT_CLAUDE_TIMEOUT_MS = 10_000;
18
+ const DEFAULT_SUBAGENT_TIMEOUT_MS = 30 * 60_000;
19
+ const DEFAULT_SUBAGENT_RETRY_MAX_ATTEMPTS = 3;
20
+ const DEFAULT_SUBAGENT_RETRY_BACKOFF_MS = 2_000;
21
+ const DEFAULT_SUBAGENT_RETRY_MAX_BACKOFF_MS = 15_000;
22
+ const DEFAULT_MODEL_RUN_TIMEOUT_MS = 110_000;
23
+ const DEFAULT_GATEWAY_CONCURRENCY = 10;
24
+ const MAX_SUBAGENT_OUTPUT_BYTES = 5_000_000;
25
+ const MAX_SUBAGENT_EVENT_PAYLOAD_BYTES = 200_000;
26
+ const MAX_GATEWAY_LOG_BYTES = 200_000;
27
+ const SUBAGENT_EVENT_BATCH_SIZE = 200;
28
+ const SUBAGENT_CANCEL_KILL_TIMEOUT_MS = 5_000;
29
+ const PROCESS_KILL_GRACE_MS = 2_000;
30
+ const VALID_ENVIRONMENTS = new Set(['local', 'dev', 'test', 'stage', 'prod']);
31
+ const CLAUDE_ENV_ALLOWLIST = [
32
+ 'PATH',
33
+ 'HOME',
34
+ 'USER',
35
+ 'LOGNAME',
36
+ 'SHELL',
37
+ 'LANG',
38
+ 'LC_ALL',
39
+ 'LC_CTYPE',
40
+ 'TERM',
41
+ 'TMPDIR',
42
+ 'TMP',
43
+ 'TEMP',
44
+ 'HTTP_PROXY',
45
+ 'HTTPS_PROXY',
46
+ 'NO_PROXY',
47
+ 'http_proxy',
48
+ 'https_proxy',
49
+ 'no_proxy',
50
+ 'XDG_CONFIG_HOME',
51
+ 'XDG_CACHE_HOME',
52
+ 'XDG_DATA_HOME',
53
+ 'SSH_AUTH_SOCK',
54
+ 'SSH_AGENT_PID',
55
+ ];
56
+ const CONFIG_DIR = process.env.PANORAMA_GATEWAY_CONFIG_DIR || path.join(os.homedir(), '.panorama');
57
+ const SUBAGENT_WORKDIR_ROOT = path.join(CONFIG_DIR, 'subagents');
58
+ const CLAUDE_GATEWAY_HOME = process.env.PANORAMA_CLAUDE_HOME || null;
59
+ const CONFIG_PATH = process.env.PANORAMA_GATEWAY_CONFIG_PATH || path.join(CONFIG_DIR, 'gateway.json');
60
+ const LOG_PATH = process.env.PANORAMA_GATEWAY_LOG_PATH || path.join(path.dirname(CONFIG_PATH), 'gateway.log');
61
+ const PID_PATH = process.env.PANORAMA_GATEWAY_PID_PATH || path.join(path.dirname(CONFIG_PATH), 'gateway.pid');
62
+ const DEFAULT_CLAUDE_PATH = path.join(os.homedir(), '.claude', 'local', 'claude');
63
+ const CLAUDE_COMMAND = process.env.PANORAMA_CLAUDE_CLI ||
64
+ process.env.CLAUDE_CLI ||
65
+ (fsSync.existsSync(DEFAULT_CLAUDE_PATH) ? DEFAULT_CLAUDE_PATH : 'claude');
66
+ const CODEX_COMMAND = process.env.PANORAMA_CODEX_CLI || process.env.CODEX_CLI || 'codex';
67
+ const GEMINI_COMMAND = process.env.PANORAMA_GEMINI_CLI || process.env.GEMINI_CLI || 'gemini';
68
+ let CLAUDE_SUPPORT = null;
69
+ let PROVIDER_CAPABILITIES = null;
70
+ const GATEWAY_CONCURRENCY = (() => {
71
+ const raw = process.env.PANORAMA_GATEWAY_CONCURRENCY;
72
+ if (!raw)
73
+ return DEFAULT_GATEWAY_CONCURRENCY;
74
+ const parsed = Number.parseInt(raw, 10);
75
+ if (!Number.isFinite(parsed) || parsed <= 0)
76
+ return DEFAULT_GATEWAY_CONCURRENCY;
77
+ return parsed;
78
+ })();
79
+ const activeSubagentRuns = new Map();
80
+ const pendingCancelByRunId = new Map();
81
+ const pendingCancelBySubagentId = new Map();
82
+ function parseArgs(argv) {
83
+ const positional = [];
84
+ const options = {};
85
+ for (let i = 0; i < argv.length; i += 1) {
86
+ const arg = argv[i];
87
+ if (!arg)
88
+ continue;
89
+ if (arg.startsWith('--')) {
90
+ const [flag, inlineValue] = arg.slice(2).split('=');
91
+ if (inlineValue !== undefined) {
92
+ options[flag] = inlineValue;
93
+ continue;
94
+ }
95
+ const next = argv[i + 1];
96
+ if (next && !next.startsWith('--')) {
97
+ options[flag] = next;
98
+ i += 1;
99
+ }
100
+ else {
101
+ options[flag] = true;
102
+ }
103
+ continue;
104
+ }
105
+ if (arg.startsWith('-') && arg.length > 1) {
106
+ options[arg.slice(1)] = true;
107
+ continue;
108
+ }
109
+ positional.push(arg);
110
+ }
111
+ const command = positional.shift() ?? null;
112
+ return { command, positional, options };
113
+ }
114
+ function printHelp() {
115
+ console.log(`\nPanorama Gateway\n\nUsage:\n panorama-gateway pair <PAIRING_CODE> [--device-name \"My Mac\"] [--supabase-url URL] [--anon-key KEY]\n panorama-gateway start [--device-name \"My Mac\"] [--daemon]\n panorama-gateway stop\n panorama-gateway logs [--lines 200] [--no-follow]\n\nEnvironment options:\n --env <local|dev|test|stage|prod> Load .env.<env> from repo root (defaults to .env)\n --env-file <path> Load a specific env file\n PANORAMA_ENV Same as --env\n PANORAMA_ENV_FILE Same as --env-file\n\nEnvironment overrides:\n PANORAMA_SUPABASE_URL or SUPABASE_URL\n PANORAMA_SUPABASE_ANON_KEY or SUPABASE_ANON_KEY or SUPABASE_PUBLISHABLE_KEY\n PANORAMA_GATEWAY_CONFIG_PATH to override config path\n PANORAMA_GATEWAY_LOG_PATH to override log path\n PANORAMA_GATEWAY_PID_PATH to override pid path\n PANORAMA_CLAUDE_CLI or CLAUDE_CLI to override claude command\n`);
116
+ }
117
+ function getStringOption(options, key) {
118
+ const value = options[key];
119
+ if (typeof value === 'string')
120
+ return value;
121
+ return undefined;
122
+ }
123
+ function normalizeEnvName(raw) {
124
+ if (!raw)
125
+ return null;
126
+ const value = raw.trim().toLowerCase();
127
+ if (!value)
128
+ return null;
129
+ if (value === 'production')
130
+ return 'prod';
131
+ if (value === 'development')
132
+ return 'local';
133
+ if (VALID_ENVIRONMENTS.has(value))
134
+ return value;
135
+ return null;
136
+ }
137
+ function findRepoRoot(startDir) {
138
+ let current = startDir;
139
+ while (true) {
140
+ if (fsSync.existsSync(path.join(current, 'pnpm-workspace.yaml'))) {
141
+ return current;
142
+ }
143
+ const parent = path.dirname(current);
144
+ if (parent === current)
145
+ return null;
146
+ current = parent;
147
+ }
148
+ }
149
+ function loadEnvironment(options) {
150
+ const envFileOption = getStringOption(options, 'env-file') ||
151
+ process.env.PANORAMA_ENV_FILE ||
152
+ process.env.PANORAMA_ENV_PATH;
153
+ const envNameOption = getStringOption(options, 'env') || process.env.PANORAMA_ENV || process.env.ENVIRONMENT;
154
+ const envName = normalizeEnvName(envNameOption) ?? 'local';
155
+ const envPath = envFileOption
156
+ ? path.resolve(envFileOption)
157
+ : path.join(findRepoRoot(process.cwd()) ?? process.cwd(), envName === 'local' ? '.env' : `.env.${envName}`);
158
+ if (!fsSync.existsSync(envPath)) {
159
+ if (envFileOption || envNameOption) {
160
+ throw new Error(`Environment file not found: ${envPath}`);
161
+ }
162
+ return;
163
+ }
164
+ dotenv.config({ path: envPath });
165
+ }
166
+ function resolveSupabaseUrl(options, config) {
167
+ return (getStringOption(options, 'supabase-url') ||
168
+ process.env.PANORAMA_SUPABASE_URL ||
169
+ process.env.SUPABASE_URL ||
170
+ config?.supabaseUrl ||
171
+ '');
172
+ }
173
+ function resolveSupabaseAnonKey(options, config) {
174
+ return (getStringOption(options, 'anon-key') ||
175
+ process.env.PANORAMA_SUPABASE_ANON_KEY ||
176
+ process.env.SUPABASE_ANON_KEY ||
177
+ process.env.SUPABASE_PUBLISHABLE_KEY ||
178
+ config?.supabaseAnonKey ||
179
+ '');
180
+ }
181
+ async function loadConfig() {
182
+ const raw = await fs.readFile(CONFIG_PATH, 'utf-8');
183
+ return JSON.parse(raw);
184
+ }
185
+ async function saveConfig(config) {
186
+ await fs.mkdir(path.dirname(CONFIG_PATH), { recursive: true });
187
+ await fs.writeFile(CONFIG_PATH, JSON.stringify(config, null, 2));
188
+ }
189
+ function logInfo(message, data) {
190
+ if (data) {
191
+ console.log(`[gateway] ${message}`, data);
192
+ }
193
+ else {
194
+ console.log(`[gateway] ${message}`);
195
+ }
196
+ }
197
+ function logError(message, data) {
198
+ if (data) {
199
+ console.error(`[gateway] ${message}`, data);
200
+ }
201
+ else {
202
+ console.error(`[gateway] ${message}`);
203
+ }
204
+ }
205
+ function readPidFile() {
206
+ try {
207
+ const raw = fsSync.readFileSync(PID_PATH, 'utf-8').trim();
208
+ const pid = Number.parseInt(raw, 10);
209
+ return Number.isFinite(pid) ? pid : null;
210
+ }
211
+ catch {
212
+ return null;
213
+ }
214
+ }
215
+ async function writePidFile(pid) {
216
+ await fs.mkdir(path.dirname(PID_PATH), { recursive: true });
217
+ await fs.writeFile(PID_PATH, `${pid}\n`);
218
+ }
219
+ async function removePidFile() {
220
+ try {
221
+ await fs.unlink(PID_PATH);
222
+ }
223
+ catch {
224
+ // ignore missing pid
225
+ }
226
+ }
227
+ function isProcessAlive(pid) {
228
+ try {
229
+ process.kill(pid, 0);
230
+ return true;
231
+ }
232
+ catch {
233
+ return false;
234
+ }
235
+ }
236
+ async function safeJson(response) {
237
+ try {
238
+ return await response.json();
239
+ }
240
+ catch {
241
+ return null;
242
+ }
243
+ }
244
+ async function pairGateway(code, options) {
245
+ const supabaseUrl = resolveSupabaseUrl(options);
246
+ const supabaseAnonKey = resolveSupabaseAnonKey(options);
247
+ if (!supabaseUrl || !supabaseAnonKey) {
248
+ throw new Error('Missing Supabase URL or anon key for pairing. Provide SUPABASE_URL/SUPABASE_ANON_KEY or use --env.');
249
+ }
250
+ const deviceName = getStringOption(options, 'device-name') || process.env.PANORAMA_GATEWAY_DEVICE_NAME || os.hostname();
251
+ const url = `${supabaseUrl.replace(/\/$/, '')}/functions/v1/exchange-gateway-pairing-code`;
252
+ const response = await fetch(url, {
253
+ method: 'POST',
254
+ headers: {
255
+ 'Content-Type': 'application/json',
256
+ apikey: supabaseAnonKey,
257
+ Authorization: `Bearer ${supabaseAnonKey}`,
258
+ },
259
+ body: JSON.stringify({
260
+ code: code.trim(),
261
+ device_name: deviceName,
262
+ }),
263
+ });
264
+ const body = await safeJson(response);
265
+ if (!response.ok || body?.success === false) {
266
+ const message = body?.error || `Pairing failed with status ${response.status}`;
267
+ throw new Error(message);
268
+ }
269
+ if (!body?.access_token || !body?.refresh_token || !body?.gateway_id || !body?.team_id) {
270
+ throw new Error('Pairing response missing required fields');
271
+ }
272
+ const config = {
273
+ supabaseUrl,
274
+ supabaseAnonKey,
275
+ accessToken: body.access_token,
276
+ refreshToken: body.refresh_token,
277
+ gatewayId: body.gateway_id,
278
+ teamId: body.team_id,
279
+ deviceName,
280
+ };
281
+ await saveConfig(config);
282
+ logInfo('Gateway paired successfully', {
283
+ gatewayId: body.gateway_id,
284
+ teamId: body.team_id,
285
+ configPath: CONFIG_PATH,
286
+ });
287
+ }
288
+ async function execClaudeVersion() {
289
+ const start = Date.now();
290
+ try {
291
+ const { stdout, stderr } = await execFileAsync(CLAUDE_COMMAND, ['--version'], {
292
+ timeout: DEFAULT_CLAUDE_TIMEOUT_MS,
293
+ });
294
+ const durationMs = Date.now() - start;
295
+ const trimmed = stdout.trim();
296
+ return {
297
+ ok: true,
298
+ stdout,
299
+ stderr,
300
+ exitCode: 0,
301
+ durationMs,
302
+ version: trimmed || undefined,
303
+ };
304
+ }
305
+ catch (error) {
306
+ const durationMs = Date.now() - start;
307
+ const err = error;
308
+ return {
309
+ ok: false,
310
+ stdout: err.stdout ? String(err.stdout) : '',
311
+ stderr: err.stderr ? String(err.stderr) : '',
312
+ exitCode: typeof err.code === 'number' ? err.code : null,
313
+ durationMs,
314
+ error: err.message,
315
+ };
316
+ }
317
+ }
318
+ async function execClaudeHelp() {
319
+ const start = Date.now();
320
+ try {
321
+ const { stdout, stderr } = await execFileAsync(CLAUDE_COMMAND, ['--help'], {
322
+ timeout: DEFAULT_CLAUDE_TIMEOUT_MS,
323
+ });
324
+ const durationMs = Date.now() - start;
325
+ return {
326
+ ok: true,
327
+ stdout,
328
+ stderr,
329
+ exitCode: 0,
330
+ durationMs,
331
+ };
332
+ }
333
+ catch (error) {
334
+ const durationMs = Date.now() - start;
335
+ const err = error;
336
+ return {
337
+ ok: false,
338
+ stdout: err.stdout ? String(err.stdout) : '',
339
+ stderr: err.stderr ? String(err.stderr) : '',
340
+ exitCode: typeof err.code === 'number' ? err.code : null,
341
+ durationMs,
342
+ error: err.message,
343
+ };
344
+ }
345
+ }
346
+ async function detectClaudeSupport() {
347
+ const help = await execClaudeHelp();
348
+ if (!help.ok) {
349
+ return DEFAULT_CLAUDE_SUPPORT;
350
+ }
351
+ return parseClaudeSupport(`${help.stdout}\n${help.stderr}`);
352
+ }
353
+ async function buildCapabilities() {
354
+ const providers = listGatewayCliProviders();
355
+ const entries = await Promise.all(providers.map(async (provider) => {
356
+ try {
357
+ const capability = await provider.detectCapabilities();
358
+ return [provider.id, capability];
359
+ }
360
+ catch (error) {
361
+ return [
362
+ provider.id,
363
+ {
364
+ available: false,
365
+ error: error instanceof Error ? error.message : String(error),
366
+ supports: { output_schema: false, stream_json: false, tool_disable: false },
367
+ },
368
+ ];
369
+ }
370
+ }));
371
+ const providerCapabilities = entries.reduce((acc, [id, capability]) => {
372
+ acc[id] = capability;
373
+ return acc;
374
+ }, {});
375
+ PROVIDER_CAPABILITIES = providerCapabilities;
376
+ const claudeCapabilities = providerCapabilities.claude_code;
377
+ if (claudeCapabilities?.supported_flags) {
378
+ CLAUDE_SUPPORT = coerceClaudeSupport(claudeCapabilities.supported_flags);
379
+ }
380
+ const claudeCliLegacy = claudeCapabilities
381
+ ? {
382
+ available: claudeCapabilities.available,
383
+ version: claudeCapabilities.version,
384
+ error: claudeCapabilities.error,
385
+ command: `${CLAUDE_COMMAND} --version`,
386
+ supported_flags: claudeCapabilities.supported_flags,
387
+ }
388
+ : {
389
+ available: false,
390
+ error: 'Claude CLI not detected',
391
+ supported_flags: DEFAULT_CLAUDE_SUPPORT,
392
+ };
393
+ return {
394
+ providers: providerCapabilities,
395
+ claude_cli: claudeCliLegacy,
396
+ platform: process.platform,
397
+ arch: process.arch,
398
+ node_version: process.version,
399
+ };
400
+ }
401
+ function appendClaudeIsolationArgs(args, support) {
402
+ if (support.settingSourcesFlag) {
403
+ args.push('--setting-sources', 'local');
404
+ }
405
+ if (support.disableSlashCommandsFlag) {
406
+ args.push('--disable-slash-commands');
407
+ }
408
+ }
409
+ async function ensureClaudeGatewayHome() {
410
+ if (!CLAUDE_GATEWAY_HOME)
411
+ return;
412
+ await fs.mkdir(CLAUDE_GATEWAY_HOME, { recursive: true });
413
+ await fs.mkdir(path.join(CLAUDE_GATEWAY_HOME, 'xdg', 'config'), { recursive: true });
414
+ await fs.mkdir(path.join(CLAUDE_GATEWAY_HOME, 'xdg', 'cache'), { recursive: true });
415
+ await fs.mkdir(path.join(CLAUDE_GATEWAY_HOME, 'xdg', 'data'), { recursive: true });
416
+ const sourceSessionEnv = path.join(os.homedir(), '.claude', 'session-env');
417
+ const destSessionEnv = path.join(CLAUDE_GATEWAY_HOME, 'session-env');
418
+ try {
419
+ const srcStat = await fs.stat(sourceSessionEnv);
420
+ if (srcStat.isDirectory()) {
421
+ try {
422
+ await fs.stat(destSessionEnv);
423
+ }
424
+ catch {
425
+ await fs.cp(sourceSessionEnv, destSessionEnv, { recursive: true });
426
+ }
427
+ }
428
+ }
429
+ catch {
430
+ // If there's no session env yet, leave it empty; Claude will prompt if needed.
431
+ }
432
+ }
433
+ function buildClaudeEnv() {
434
+ const env = {};
435
+ for (const key of CLAUDE_ENV_ALLOWLIST) {
436
+ const value = process.env[key];
437
+ if (value !== undefined) {
438
+ env[key] = value;
439
+ }
440
+ }
441
+ if (CLAUDE_GATEWAY_HOME) {
442
+ env.HOME = CLAUDE_GATEWAY_HOME;
443
+ env.XDG_CONFIG_HOME = path.join(CLAUDE_GATEWAY_HOME, 'xdg', 'config');
444
+ env.XDG_CACHE_HOME = path.join(CLAUDE_GATEWAY_HOME, 'xdg', 'cache');
445
+ env.XDG_DATA_HOME = path.join(CLAUDE_GATEWAY_HOME, 'xdg', 'data');
446
+ }
447
+ return env;
448
+ }
449
+ async function runSubagentCommand(command, args, options) {
450
+ const start = Date.now();
451
+ const timeoutMs = options.timeoutMs ?? DEFAULT_SUBAGENT_TIMEOUT_MS;
452
+ return await new Promise((resolve) => {
453
+ const stdoutChunks = [];
454
+ const stderrChunks = [];
455
+ let stdoutBytes = 0;
456
+ let stderrBytes = 0;
457
+ let stdoutTruncated = false;
458
+ let stderrTruncated = false;
459
+ let timedOut = false;
460
+ const child = spawn(command, args, {
461
+ cwd: options.cwd,
462
+ env: options.env ?? buildClaudeEnv(),
463
+ stdio: ['ignore', 'pipe', 'pipe'],
464
+ });
465
+ options.onStart?.(child);
466
+ child.stdout?.on('data', (chunk) => {
467
+ if (stdoutBytes >= MAX_SUBAGENT_OUTPUT_BYTES) {
468
+ stdoutTruncated = true;
469
+ return;
470
+ }
471
+ const remaining = MAX_SUBAGENT_OUTPUT_BYTES - stdoutBytes;
472
+ if (chunk.length > remaining) {
473
+ stdoutChunks.push(chunk.slice(0, remaining));
474
+ stdoutBytes = MAX_SUBAGENT_OUTPUT_BYTES;
475
+ stdoutTruncated = true;
476
+ return;
477
+ }
478
+ stdoutChunks.push(chunk);
479
+ stdoutBytes += chunk.length;
480
+ });
481
+ child.stderr?.on('data', (chunk) => {
482
+ if (stderrBytes >= MAX_SUBAGENT_OUTPUT_BYTES) {
483
+ stderrTruncated = true;
484
+ return;
485
+ }
486
+ const remaining = MAX_SUBAGENT_OUTPUT_BYTES - stderrBytes;
487
+ if (chunk.length > remaining) {
488
+ stderrChunks.push(chunk.slice(0, remaining));
489
+ stderrBytes = MAX_SUBAGENT_OUTPUT_BYTES;
490
+ stderrTruncated = true;
491
+ return;
492
+ }
493
+ stderrChunks.push(chunk);
494
+ stderrBytes += chunk.length;
495
+ });
496
+ const timer = timeoutMs && Number.isFinite(timeoutMs)
497
+ ? setTimeout(() => {
498
+ timedOut = true;
499
+ if (!child.killed) {
500
+ child.kill('SIGTERM');
501
+ setTimeout(() => {
502
+ if (!child.killed) {
503
+ child.kill('SIGKILL');
504
+ }
505
+ }, PROCESS_KILL_GRACE_MS);
506
+ }
507
+ }, timeoutMs)
508
+ : null;
509
+ const finalize = (exitCode, error) => {
510
+ if (timer)
511
+ clearTimeout(timer);
512
+ const durationMs = Date.now() - start;
513
+ const stdout = Buffer.concat(stdoutChunks).toString('utf-8');
514
+ const stderr = Buffer.concat(stderrChunks).toString('utf-8');
515
+ const ok = !timedOut && exitCode === 0 && !error;
516
+ resolve({
517
+ ok,
518
+ stdout,
519
+ stderr,
520
+ exitCode,
521
+ durationMs,
522
+ timedOut,
523
+ stdoutTruncated,
524
+ stderrTruncated,
525
+ error,
526
+ });
527
+ };
528
+ child.on('error', (error) => {
529
+ finalize(null, error.message);
530
+ });
531
+ child.on('close', (code) => {
532
+ finalize(code ?? null);
533
+ });
534
+ });
535
+ }
536
+ async function runSubagentCommandStreaming(command, args, options) {
537
+ const start = Date.now();
538
+ const timeoutMs = options.timeoutMs ?? DEFAULT_SUBAGENT_TIMEOUT_MS;
539
+ return await new Promise((resolve) => {
540
+ const stdoutChunks = [];
541
+ const stderrChunks = [];
542
+ const events = [];
543
+ let stdoutBytes = 0;
544
+ let stderrBytes = 0;
545
+ let stdoutTruncated = false;
546
+ let stderrTruncated = false;
547
+ let timedOut = false;
548
+ let buffer = '';
549
+ let sequence = 0;
550
+ const child = spawn(command, args, {
551
+ cwd: options.cwd,
552
+ env: options.env ?? buildClaudeEnv(),
553
+ stdio: ['ignore', 'pipe', 'pipe'],
554
+ });
555
+ options.onStart?.(child);
556
+ const captureStdout = (chunk) => {
557
+ if (stdoutBytes >= MAX_SUBAGENT_OUTPUT_BYTES) {
558
+ stdoutTruncated = true;
559
+ return;
560
+ }
561
+ const remaining = MAX_SUBAGENT_OUTPUT_BYTES - stdoutBytes;
562
+ if (chunk.length > remaining) {
563
+ stdoutChunks.push(chunk.slice(0, remaining));
564
+ stdoutBytes = MAX_SUBAGENT_OUTPUT_BYTES;
565
+ stdoutTruncated = true;
566
+ return;
567
+ }
568
+ stdoutChunks.push(chunk);
569
+ stdoutBytes += chunk.length;
570
+ };
571
+ const parseLine = (line) => {
572
+ const trimmed = line.trim();
573
+ if (!trimmed)
574
+ return;
575
+ let event = null;
576
+ try {
577
+ event = JSON.parse(trimmed);
578
+ }
579
+ catch {
580
+ event = null;
581
+ }
582
+ events.push({
583
+ sequence,
584
+ raw: trimmed,
585
+ event,
586
+ });
587
+ sequence += 1;
588
+ };
589
+ child.stdout?.on('data', (chunk) => {
590
+ captureStdout(chunk);
591
+ buffer += chunk.toString('utf-8');
592
+ let newlineIndex = buffer.indexOf('\n');
593
+ while (newlineIndex >= 0) {
594
+ const line = buffer.slice(0, newlineIndex);
595
+ buffer = buffer.slice(newlineIndex + 1);
596
+ parseLine(line);
597
+ newlineIndex = buffer.indexOf('\n');
598
+ }
599
+ });
600
+ child.stderr?.on('data', (chunk) => {
601
+ if (stderrBytes >= MAX_SUBAGENT_OUTPUT_BYTES) {
602
+ stderrTruncated = true;
603
+ return;
604
+ }
605
+ const remaining = MAX_SUBAGENT_OUTPUT_BYTES - stderrBytes;
606
+ if (chunk.length > remaining) {
607
+ stderrChunks.push(chunk.slice(0, remaining));
608
+ stderrBytes = MAX_SUBAGENT_OUTPUT_BYTES;
609
+ stderrTruncated = true;
610
+ return;
611
+ }
612
+ stderrChunks.push(chunk);
613
+ stderrBytes += chunk.length;
614
+ });
615
+ const timer = timeoutMs && Number.isFinite(timeoutMs)
616
+ ? setTimeout(() => {
617
+ timedOut = true;
618
+ if (!child.killed) {
619
+ child.kill('SIGTERM');
620
+ setTimeout(() => {
621
+ if (!child.killed) {
622
+ child.kill('SIGKILL');
623
+ }
624
+ }, PROCESS_KILL_GRACE_MS);
625
+ }
626
+ }, timeoutMs)
627
+ : null;
628
+ const finalize = (exitCode, error) => {
629
+ if (timer)
630
+ clearTimeout(timer);
631
+ if (buffer.trim().length > 0) {
632
+ parseLine(buffer);
633
+ }
634
+ const durationMs = Date.now() - start;
635
+ const stdout = Buffer.concat(stdoutChunks).toString('utf-8');
636
+ const stderr = Buffer.concat(stderrChunks).toString('utf-8');
637
+ const ok = !timedOut && exitCode === 0 && !error;
638
+ resolve({
639
+ ok,
640
+ stdout,
641
+ stderr,
642
+ exitCode,
643
+ durationMs,
644
+ timedOut,
645
+ stdoutTruncated,
646
+ stderrTruncated,
647
+ events,
648
+ error,
649
+ });
650
+ };
651
+ child.on('error', (error) => {
652
+ finalize(null, error.message);
653
+ });
654
+ child.on('close', (code) => {
655
+ finalize(code ?? null);
656
+ });
657
+ });
658
+ }
659
+ function extractSubagentPrompt(input) {
660
+ if (!input)
661
+ return null;
662
+ const raw = (typeof input.prompt === 'string' && input.prompt) ||
663
+ (typeof input.query === 'string' && input.query) ||
664
+ (typeof input.task === 'string' && input.task);
665
+ if (!raw)
666
+ return null;
667
+ const trimmed = raw.trim();
668
+ return trimmed.length > 0 ? trimmed : null;
669
+ }
670
+ function isUuid(value) {
671
+ return /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value);
672
+ }
673
+ async function ensureSubagentWorkDir(subagentId) {
674
+ const dir = path.join(SUBAGENT_WORKDIR_ROOT, subagentId);
675
+ await fs.mkdir(dir, { recursive: true });
676
+ return dir;
677
+ }
678
+ function trimEventPayload(event) {
679
+ try {
680
+ const raw = JSON.stringify(event);
681
+ if (raw.length <= MAX_SUBAGENT_EVENT_PAYLOAD_BYTES)
682
+ return event;
683
+ return {
684
+ truncated: true,
685
+ original_bytes: raw.length,
686
+ preview: raw.slice(0, MAX_SUBAGENT_EVENT_PAYLOAD_BYTES),
687
+ };
688
+ }
689
+ catch {
690
+ return event;
691
+ }
692
+ }
693
+ function truncateLog(value) {
694
+ if (!value) {
695
+ return { value: '', truncated: false, originalBytes: 0 };
696
+ }
697
+ if (value.length <= MAX_GATEWAY_LOG_BYTES) {
698
+ return { value, truncated: false, originalBytes: value.length };
699
+ }
700
+ return {
701
+ value: value.slice(0, MAX_GATEWAY_LOG_BYTES),
702
+ truncated: true,
703
+ originalBytes: value.length,
704
+ };
705
+ }
706
+ function extractFirstJsonObject(text) {
707
+ let inString = false;
708
+ let escape = false;
709
+ for (let i = 0; i < text.length; i += 1) {
710
+ const char = text[i];
711
+ if (escape) {
712
+ escape = false;
713
+ continue;
714
+ }
715
+ if (char === '\\\\') {
716
+ if (inString)
717
+ escape = true;
718
+ continue;
719
+ }
720
+ if (char === '"') {
721
+ inString = !inString;
722
+ continue;
723
+ }
724
+ if (inString)
725
+ continue;
726
+ if (char !== '{')
727
+ continue;
728
+ let depth = 0;
729
+ let innerInString = false;
730
+ let innerEscape = false;
731
+ for (let j = i; j < text.length; j += 1) {
732
+ const inner = text[j];
733
+ if (innerEscape) {
734
+ innerEscape = false;
735
+ continue;
736
+ }
737
+ if (inner === '\\\\') {
738
+ if (innerInString)
739
+ innerEscape = true;
740
+ continue;
741
+ }
742
+ if (inner === '"') {
743
+ innerInString = !innerInString;
744
+ continue;
745
+ }
746
+ if (innerInString)
747
+ continue;
748
+ if (inner === '{')
749
+ depth += 1;
750
+ if (inner === '}') {
751
+ depth -= 1;
752
+ if (depth === 0) {
753
+ const candidate = text.slice(i, j + 1);
754
+ try {
755
+ return JSON.parse(candidate);
756
+ }
757
+ catch {
758
+ break;
759
+ }
760
+ }
761
+ }
762
+ }
763
+ }
764
+ return null;
765
+ }
766
+ function extractGatewayErrorSummary(stderr) {
767
+ if (!stderr)
768
+ return null;
769
+ const parsed = extractFirstJsonObject(stderr);
770
+ if (!parsed)
771
+ return null;
772
+ const error = parsed.error;
773
+ if (error && typeof error === 'object') {
774
+ return {
775
+ message: typeof error.message === 'string' ? error.message : undefined,
776
+ type: typeof error.type === 'string' ? error.type : undefined,
777
+ code: typeof error.code === 'string' ? error.code : undefined,
778
+ param: typeof error.param === 'string' ? error.param : undefined,
779
+ };
780
+ }
781
+ return {
782
+ message: typeof parsed.message === 'string' ? parsed.message : undefined,
783
+ type: typeof parsed.type === 'string' ? String(parsed.type) : undefined,
784
+ code: typeof parsed.code === 'string' ? String(parsed.code) : undefined,
785
+ };
786
+ }
787
+ function describeGatewayJob(job) {
788
+ if (job.job_type === 'model_run') {
789
+ const payload = job.payload ?? {};
790
+ const model = typeof payload.model === 'string' ? payload.model : null;
791
+ const provider = typeof payload.provider === 'string'
792
+ ? payload.provider
793
+ : model
794
+ ? inferGatewayProviderFromModel(model)
795
+ : null;
796
+ return { job_kind: 'cycle', provider, model };
797
+ }
798
+ if (job.job_type === 'subagent_run') {
799
+ const payload = job.payload ?? {};
800
+ const subagentType = typeof payload.subagent_type === 'string' ? payload.subagent_type : null;
801
+ const provider = inferProviderFromSubagentType(subagentType);
802
+ return { job_kind: 'subagent', provider, subagent_type: subagentType };
803
+ }
804
+ if (job.job_type === 'subagent_cancel') {
805
+ return { job_kind: 'subagent_cancel' };
806
+ }
807
+ return { job_kind: job.job_type };
808
+ }
809
+ function inferProviderFromSubagentType(subagentType) {
810
+ if (!subagentType)
811
+ return null;
812
+ const lowered = subagentType.toLowerCase();
813
+ if (lowered.includes('claude'))
814
+ return 'claude_code';
815
+ if (lowered.includes('codex'))
816
+ return 'codex';
817
+ if (lowered.includes('gemini'))
818
+ return 'gemini';
819
+ return null;
820
+ }
821
+ function resolveSubagentRetryConfig(config) {
822
+ const rawMaxAttempts = typeof config.max_attempts === 'number' ? config.max_attempts : null;
823
+ const rawBackoffMs = typeof config.retry_backoff_ms === 'number' ? config.retry_backoff_ms : null;
824
+ const rawMaxBackoffMs = typeof config.retry_max_backoff_ms === 'number' ? config.retry_max_backoff_ms : null;
825
+ const maxAttempts = rawMaxAttempts && Number.isFinite(rawMaxAttempts) && rawMaxAttempts > 0
826
+ ? Math.floor(rawMaxAttempts)
827
+ : DEFAULT_SUBAGENT_RETRY_MAX_ATTEMPTS;
828
+ const backoffMs = rawBackoffMs && Number.isFinite(rawBackoffMs) && rawBackoffMs > 0
829
+ ? rawBackoffMs
830
+ : DEFAULT_SUBAGENT_RETRY_BACKOFF_MS;
831
+ const maxBackoffMs = rawMaxBackoffMs && Number.isFinite(rawMaxBackoffMs) && rawMaxBackoffMs > 0
832
+ ? rawMaxBackoffMs
833
+ : DEFAULT_SUBAGENT_RETRY_MAX_BACKOFF_MS;
834
+ return {
835
+ maxAttempts,
836
+ backoffMs,
837
+ maxBackoffMs,
838
+ retryOnCapacity: config.retry_on_capacity !== false,
839
+ retryOnTimeout: config.retry_on_timeout !== false,
840
+ retryOnNetwork: config.retry_on_network !== false,
841
+ retryOnParse: config.retry_on_parse !== false,
842
+ };
843
+ }
844
+ function computeRetryDelayMs(attempt, baseMs, maxMs) {
845
+ const exponent = Math.max(0, attempt - 1);
846
+ const raw = Math.min(maxMs, baseMs * Math.pow(2, exponent));
847
+ const jitter = 0.7 + Math.random() * 0.6;
848
+ return Math.round(raw * jitter);
849
+ }
850
+ async function sleep(ms) {
851
+ if (!ms || ms <= 0)
852
+ return;
853
+ await new Promise((resolve) => setTimeout(resolve, ms));
854
+ }
855
+ function classifySubagentRetryable(params) {
856
+ const { result, parseError, parseStrict, config } = params;
857
+ if (parseStrict && parseError && config.retryOnParse) {
858
+ return { retryable: true, reason: 'parse_error' };
859
+ }
860
+ if (!result.ok) {
861
+ if (result.timedOut && config.retryOnTimeout) {
862
+ return { retryable: true, reason: 'timeout' };
863
+ }
864
+ const message = `${result.error ?? ''}\n${result.stderr ?? ''}`.toLowerCase();
865
+ if (config.retryOnCapacity) {
866
+ if (message.includes('resource_exhausted') ||
867
+ message.includes('capacity') ||
868
+ message.includes('rate limit') ||
869
+ message.includes('too many requests') ||
870
+ message.includes('429')) {
871
+ return { retryable: true, reason: 'capacity' };
872
+ }
873
+ }
874
+ if (config.retryOnNetwork) {
875
+ if (message.includes('econnreset') ||
876
+ message.includes('etimedout') ||
877
+ message.includes('enotfound') ||
878
+ message.includes('eai_again') ||
879
+ message.includes('socket hang up') ||
880
+ message.includes('network')) {
881
+ return { retryable: true, reason: 'network' };
882
+ }
883
+ }
884
+ }
885
+ return null;
886
+ }
887
+ async function getNextRunSequence(supabase, subagentId) {
888
+ const { data } = await supabase
889
+ .from('subagent_runs')
890
+ .select('run_sequence')
891
+ .eq('subagent_id', subagentId)
892
+ .order('run_sequence', { ascending: false })
893
+ .limit(1)
894
+ .maybeSingle();
895
+ const lastSequence = data && typeof data.run_sequence === 'number' ? data.run_sequence : 0;
896
+ return lastSequence + 1;
897
+ }
898
+ function mergeMetadata(base, update) {
899
+ return {
900
+ ...(base ?? {}),
901
+ ...update,
902
+ };
903
+ }
904
+ function asJson(value) {
905
+ return value;
906
+ }
907
+ function requestSubagentCancel(subagentId, runId, reason) {
908
+ const activeRun = activeSubagentRuns.get(subagentId);
909
+ if (activeRun && (!runId || activeRun.runId === runId)) {
910
+ activeRun.cancelled = true;
911
+ activeRun.cancelReason = reason;
912
+ if (activeRun.child) {
913
+ activeRun.child.kill('SIGTERM');
914
+ setTimeout(() => {
915
+ if (!activeRun.child?.killed) {
916
+ activeRun.child?.kill('SIGKILL');
917
+ }
918
+ }, SUBAGENT_CANCEL_KILL_TIMEOUT_MS);
919
+ }
920
+ return { active: true };
921
+ }
922
+ if (runId) {
923
+ pendingCancelByRunId.set(runId, { reason });
924
+ }
925
+ else {
926
+ pendingCancelBySubagentId.set(subagentId, { reason });
927
+ }
928
+ return { active: false };
929
+ }
930
+ function resolvePendingCancel(subagentId, runId, metadata) {
931
+ const cancelRunId = typeof metadata.cancel_run_id === 'string' ? metadata.cancel_run_id : null;
932
+ if (cancelRunId && cancelRunId === runId) {
933
+ return typeof metadata.cancel_reason === 'string'
934
+ ? metadata.cancel_reason
935
+ : 'Cancelled';
936
+ }
937
+ const pendingByRun = pendingCancelByRunId.get(runId);
938
+ if (pendingByRun)
939
+ return pendingByRun.reason;
940
+ const pendingBySubagent = pendingCancelBySubagentId.get(subagentId);
941
+ if (pendingBySubagent)
942
+ return pendingBySubagent.reason;
943
+ return null;
944
+ }
945
+ function clearPendingCancel(subagentId, runId) {
946
+ pendingCancelByRunId.delete(runId);
947
+ pendingCancelBySubagentId.delete(subagentId);
948
+ }
949
+ function buildErrorOutput(message) {
950
+ return {
951
+ schema_version: SUBAGENT_SCHEMA_VERSION,
952
+ content: message,
953
+ content_type: 'text',
954
+ errors: [message],
955
+ };
956
+ }
957
+ function isJobTargeted(job, gatewayId) {
958
+ return !job.gateway_id || job.gateway_id === gatewayId;
959
+ }
960
+ async function sendHeartbeat(supabase, status, capabilities, deviceName) {
961
+ const { error } = await supabase.functions.invoke('gateway-heartbeat', {
962
+ body: {
963
+ status,
964
+ capabilities,
965
+ device_name: deviceName,
966
+ },
967
+ });
968
+ if (error) {
969
+ logError('Failed to send gateway heartbeat', { error: error.message });
970
+ }
971
+ }
972
+ async function claimJob(supabase, config, job) {
973
+ if (!isJobTargeted(job, config.gatewayId)) {
974
+ return null;
975
+ }
976
+ const { data, error } = await supabase
977
+ .from('gateway_jobs')
978
+ .update({
979
+ status: 'running',
980
+ gateway_id: config.gatewayId,
981
+ started_at: new Date().toISOString(),
982
+ })
983
+ .eq('id', job.id)
984
+ .eq('status', 'queued')
985
+ .or(`gateway_id.is.null,gateway_id.eq.${config.gatewayId}`)
986
+ .select('*')
987
+ .maybeSingle();
988
+ if (error) {
989
+ logError('Failed to claim gateway job', { error: error.message, jobId: job.id });
990
+ return null;
991
+ }
992
+ if (!data) {
993
+ return null;
994
+ }
995
+ return data;
996
+ }
997
+ async function completeJob(supabase, jobId, status, result, errorMessage) {
998
+ const updates = {
999
+ status,
1000
+ result: result ? asJson(result) : null,
1001
+ error: errorMessage || null,
1002
+ completed_at: new Date().toISOString(),
1003
+ };
1004
+ const { error } = await supabase
1005
+ .from('gateway_jobs')
1006
+ .update(updates)
1007
+ .eq('id', jobId)
1008
+ .eq('status', 'running');
1009
+ if (error) {
1010
+ logError('Failed to update gateway job result', { error: error.message, jobId });
1011
+ }
1012
+ }
1013
+ async function updateJobDebug(supabase, jobId, debug) {
1014
+ const { error } = await supabase
1015
+ .from('gateway_jobs')
1016
+ .update({ debug: asJson(debug) })
1017
+ .eq('id', jobId);
1018
+ if (error) {
1019
+ logError('Failed to update gateway job debug info', { error: error.message, jobId });
1020
+ }
1021
+ }
1022
+ async function handleDiagnosticJob() {
1023
+ const output = await execClaudeVersion();
1024
+ if (!output.ok) {
1025
+ return {
1026
+ ok: false,
1027
+ result: {
1028
+ command: `${CLAUDE_COMMAND} --version`,
1029
+ stdout: output.stdout,
1030
+ stderr: output.stderr,
1031
+ exit_code: output.exitCode,
1032
+ duration_ms: output.durationMs,
1033
+ error: output.error,
1034
+ },
1035
+ error: output.error || 'Claude CLI check failed',
1036
+ };
1037
+ }
1038
+ return {
1039
+ ok: true,
1040
+ result: {
1041
+ command: `${CLAUDE_COMMAND} --version`,
1042
+ stdout: output.stdout,
1043
+ stderr: output.stderr,
1044
+ exit_code: output.exitCode,
1045
+ duration_ms: output.durationMs,
1046
+ version: output.version,
1047
+ },
1048
+ };
1049
+ }
1050
+ async function handleSubagentCancelJob(supabase, job) {
1051
+ const payload = job.payload ?? {};
1052
+ const subagentId = typeof payload.subagent_id === 'string'
1053
+ ? payload.subagent_id
1054
+ : typeof payload.subagentId === 'string'
1055
+ ? payload.subagentId
1056
+ : null;
1057
+ if (!subagentId) {
1058
+ return {
1059
+ ok: false,
1060
+ result: { message: 'Missing subagent_id in payload' },
1061
+ error: 'Missing subagent_id in payload',
1062
+ };
1063
+ }
1064
+ const runId = typeof payload.run_id === 'string'
1065
+ ? payload.run_id
1066
+ : typeof payload.runId === 'string'
1067
+ ? payload.runId
1068
+ : null;
1069
+ const reason = typeof payload.reason === 'string' && payload.reason.trim().length > 0
1070
+ ? payload.reason.trim()
1071
+ : 'Cancelled';
1072
+ const { data: subagent } = await supabase
1073
+ .from('subagents')
1074
+ .select('status, metadata, input')
1075
+ .eq('id', subagentId)
1076
+ .maybeSingle();
1077
+ if (!subagent || subagent.status !== 'running') {
1078
+ return {
1079
+ ok: true,
1080
+ result: {
1081
+ subagent_id: subagentId,
1082
+ run_id: runId,
1083
+ cancelled_active_run: false,
1084
+ reason,
1085
+ message: 'Subagent not running',
1086
+ },
1087
+ };
1088
+ }
1089
+ const metadata = (subagent.metadata ?? {});
1090
+ const input = (subagent.input ?? {});
1091
+ const resolvedRunId = runId ||
1092
+ (typeof metadata.current_run_id === 'string' ? metadata.current_run_id : null) ||
1093
+ (typeof input.run_id === 'string' ? input.run_id : null);
1094
+ const { active } = requestSubagentCancel(subagentId, resolvedRunId, reason);
1095
+ if (resolvedRunId) {
1096
+ const nowIso = new Date().toISOString();
1097
+ const cancelledOutput = buildErrorOutput(reason);
1098
+ const { data: runningRun, error: runningRunError } = await supabase
1099
+ .from('subagent_runs')
1100
+ .select('id, status')
1101
+ .eq('id', resolvedRunId)
1102
+ .eq('status', 'running')
1103
+ .maybeSingle();
1104
+ if (runningRunError) {
1105
+ logError('Failed to fetch running subagent run for cancel', {
1106
+ error: runningRunError.message,
1107
+ subagentId,
1108
+ runId: resolvedRunId,
1109
+ });
1110
+ }
1111
+ if (runningRun?.id) {
1112
+ const { error: runUpdateError } = await supabase
1113
+ .from('subagent_runs')
1114
+ .update({
1115
+ status: 'cancelled',
1116
+ output: asJson(cancelledOutput),
1117
+ error: reason,
1118
+ completed_at: nowIso,
1119
+ })
1120
+ .eq('id', resolvedRunId);
1121
+ if (runUpdateError) {
1122
+ logError('Failed to mark subagent run as cancelled', {
1123
+ error: runUpdateError.message,
1124
+ subagentId,
1125
+ runId: resolvedRunId,
1126
+ });
1127
+ }
1128
+ const { error: subagentUpdateError } = await supabase
1129
+ .from('subagents')
1130
+ .update({
1131
+ status: 'failed',
1132
+ output: asJson(cancelledOutput),
1133
+ error: reason,
1134
+ completed_at: nowIso,
1135
+ metadata: asJson(mergeMetadata(metadata, {
1136
+ cancel_run_id: resolvedRunId,
1137
+ cancel_requested_at: nowIso,
1138
+ cancel_reason: reason,
1139
+ })),
1140
+ })
1141
+ .eq('id', subagentId);
1142
+ if (subagentUpdateError) {
1143
+ logError('Failed to mark subagent as cancelled', {
1144
+ error: subagentUpdateError.message,
1145
+ subagentId,
1146
+ runId: resolvedRunId,
1147
+ });
1148
+ }
1149
+ }
1150
+ }
1151
+ return {
1152
+ ok: true,
1153
+ result: {
1154
+ subagent_id: subagentId,
1155
+ run_id: resolvedRunId,
1156
+ cancelled_active_run: active,
1157
+ reason,
1158
+ },
1159
+ };
1160
+ }
1161
+ async function handleSubagentRunJob(supabase, job) {
1162
+ const payload = job.payload ?? {};
1163
+ const subagentId = typeof payload.subagent_id === 'string'
1164
+ ? payload.subagent_id
1165
+ : typeof payload.subagentId === 'string'
1166
+ ? payload.subagentId
1167
+ : null;
1168
+ if (!subagentId) {
1169
+ return {
1170
+ ok: false,
1171
+ result: { message: 'Missing subagent_id in payload' },
1172
+ error: 'Missing subagent_id in payload',
1173
+ };
1174
+ }
1175
+ const { data: subagent, error: subagentError } = await supabase
1176
+ .from('subagents')
1177
+ .select('id, team_id, gateway_id, subagent_type, status, config, input, metadata')
1178
+ .eq('id', subagentId)
1179
+ .maybeSingle();
1180
+ if (subagentError || !subagent) {
1181
+ return {
1182
+ ok: false,
1183
+ result: { message: 'Subagent not found', subagent_id: subagentId },
1184
+ error: subagentError?.message ?? 'Subagent not found',
1185
+ };
1186
+ }
1187
+ const subagentType = subagent.subagent_type;
1188
+ const adapter = getSubagentAdapter(subagentType);
1189
+ if (!adapter) {
1190
+ return {
1191
+ ok: false,
1192
+ result: { message: 'Unsupported subagent type', subagent_type: subagentType },
1193
+ error: `Unsupported subagent type: ${subagentType}`,
1194
+ };
1195
+ }
1196
+ logInfo('Processing subagent run', {
1197
+ jobId: job.id,
1198
+ subagentId: subagentId,
1199
+ subagentType,
1200
+ provider: inferProviderFromSubagentType(subagentType),
1201
+ });
1202
+ const prompt = extractSubagentPrompt((subagent.input ?? {}));
1203
+ if (!prompt) {
1204
+ return {
1205
+ ok: false,
1206
+ result: { message: 'Missing prompt in subagent input', subagent_id: subagentId },
1207
+ error: 'Missing prompt in subagent input',
1208
+ };
1209
+ }
1210
+ const configPayload = (subagent.config ?? {});
1211
+ const metadata = (subagent.metadata ?? {});
1212
+ const inputPayload = (subagent.input ?? {});
1213
+ const inputRunId = typeof inputPayload.run_id === 'string' ? inputPayload.run_id.trim() : null;
1214
+ const rawRunSequence = typeof inputPayload.run_sequence === 'number'
1215
+ ? inputPayload.run_sequence
1216
+ : typeof inputPayload.runSequence === 'number'
1217
+ ? inputPayload.runSequence
1218
+ : typeof inputPayload.run_sequence === 'string'
1219
+ ? Number.parseInt(inputPayload.run_sequence, 10)
1220
+ : typeof inputPayload.runSequence === 'string'
1221
+ ? Number.parseInt(inputPayload.runSequence, 10)
1222
+ : NaN;
1223
+ const inputRunSequence = Number.isFinite(rawRunSequence) ? rawRunSequence : null;
1224
+ const resumeSessionId = typeof metadata.session_id === 'string' ? metadata.session_id.trim() : null;
1225
+ const retryConfig = resolveSubagentRetryConfig(configPayload);
1226
+ const retryReasons = [];
1227
+ if (resumeSessionId && !adapter.supportsContinuation) {
1228
+ return {
1229
+ ok: false,
1230
+ result: { message: 'Subagent does not support continuation', subagent_id: subagentId },
1231
+ error: 'Subagent provider does not support continuation',
1232
+ };
1233
+ }
1234
+ const resolvedRunId = inputRunId && isUuid(inputRunId) ? inputRunId : randomUUID();
1235
+ const nowIso = new Date().toISOString();
1236
+ const runSequence = inputRunSequence ?? (await getNextRunSequence(supabase, subagentId));
1237
+ const runMetadata = {
1238
+ schema_version: SUBAGENT_SCHEMA_VERSION,
1239
+ runner: 'gateway',
1240
+ adapter_id: adapter.id,
1241
+ subagent_type: subagentType,
1242
+ run_id: resolvedRunId,
1243
+ run_sequence: runSequence,
1244
+ };
1245
+ const pendingCancelReason = resolvePendingCancel(subagentId, resolvedRunId, metadata);
1246
+ if (pendingCancelReason) {
1247
+ clearPendingCancel(subagentId, resolvedRunId);
1248
+ const nowIso = new Date().toISOString();
1249
+ const cancelledOutput = buildErrorOutput(pendingCancelReason);
1250
+ const { error: cancelInsertError } = await supabase.from('subagent_runs').insert({
1251
+ id: resolvedRunId,
1252
+ subagent_id: subagentId,
1253
+ team_id: subagent.team_id,
1254
+ gateway_id: subagent.gateway_id,
1255
+ run_sequence: runSequence,
1256
+ status: 'cancelled',
1257
+ prompt,
1258
+ output: asJson(cancelledOutput),
1259
+ error: pendingCancelReason,
1260
+ started_at: nowIso,
1261
+ completed_at: nowIso,
1262
+ metadata: asJson({
1263
+ ...runMetadata,
1264
+ cancel_reason: pendingCancelReason,
1265
+ }),
1266
+ });
1267
+ if (cancelInsertError) {
1268
+ return {
1269
+ ok: false,
1270
+ result: { message: 'Failed to record cancelled run', subagent_id: subagentId },
1271
+ error: cancelInsertError.message,
1272
+ };
1273
+ }
1274
+ return {
1275
+ ok: true,
1276
+ result: {
1277
+ subagent_id: subagentId,
1278
+ status: 'cancelled',
1279
+ output: cancelledOutput,
1280
+ error: pendingCancelReason,
1281
+ },
1282
+ };
1283
+ }
1284
+ const { error: runInsertError } = await supabase.from('subagent_runs').insert({
1285
+ id: resolvedRunId,
1286
+ subagent_id: subagentId,
1287
+ team_id: subagent.team_id,
1288
+ gateway_id: subagent.gateway_id,
1289
+ run_sequence: runSequence,
1290
+ status: 'running',
1291
+ prompt,
1292
+ started_at: nowIso,
1293
+ metadata: asJson(runMetadata),
1294
+ });
1295
+ if (runInsertError) {
1296
+ return {
1297
+ ok: false,
1298
+ result: { message: 'Failed to create subagent run', subagent_id: subagentId },
1299
+ error: runInsertError.message,
1300
+ };
1301
+ }
1302
+ const activeRun = {
1303
+ runId: resolvedRunId,
1304
+ cancelled: false,
1305
+ };
1306
+ activeSubagentRuns.set(subagentId, activeRun);
1307
+ await supabase
1308
+ .from('subagents')
1309
+ .update({
1310
+ status: 'running',
1311
+ started_at: nowIso,
1312
+ metadata: asJson(mergeMetadata(metadata, {
1313
+ current_run_id: resolvedRunId,
1314
+ current_run_sequence: runSequence,
1315
+ })),
1316
+ })
1317
+ .eq('id', subagentId);
1318
+ const failRun = async (message) => {
1319
+ const failureOutput = buildErrorOutput(message);
1320
+ const completedAt = new Date().toISOString();
1321
+ await supabase
1322
+ .from('subagent_runs')
1323
+ .update({
1324
+ status: 'failed',
1325
+ output: asJson(failureOutput),
1326
+ error: message,
1327
+ completed_at: completedAt,
1328
+ })
1329
+ .eq('id', resolvedRunId);
1330
+ await supabase
1331
+ .from('subagents')
1332
+ .update({
1333
+ status: 'failed',
1334
+ output: asJson(failureOutput),
1335
+ error: message,
1336
+ completed_at: completedAt,
1337
+ })
1338
+ .eq('id', subagentId);
1339
+ activeSubagentRuns.delete(subagentId);
1340
+ return {
1341
+ ok: false,
1342
+ result: { message, subagent_id: subagentId },
1343
+ error: message,
1344
+ };
1345
+ };
1346
+ let runPlan;
1347
+ try {
1348
+ const adapterId = adapter.id;
1349
+ const providerSupport = PROVIDER_CAPABILITIES?.[adapterId]?.supported_flags ??
1350
+ (adapterId === 'claude_code'
1351
+ ? toFlagRecord(CLAUDE_SUPPORT ?? DEFAULT_CLAUDE_SUPPORT)
1352
+ : {});
1353
+ const command = adapterId === 'codex'
1354
+ ? CODEX_COMMAND
1355
+ : adapterId === 'gemini'
1356
+ ? GEMINI_COMMAND
1357
+ : CLAUDE_COMMAND;
1358
+ runPlan = adapter.buildRunPlan({
1359
+ prompt,
1360
+ config: configPayload,
1361
+ command,
1362
+ support: providerSupport,
1363
+ resumeSessionId,
1364
+ });
1365
+ }
1366
+ catch (error) {
1367
+ const message = error instanceof Error ? error.message : 'Failed to build subagent run plan';
1368
+ return await failRun(message);
1369
+ }
1370
+ if (runPlan.outputFormat === 'stream-json') {
1371
+ if (!adapter.normalizeStreamingResult || !adapter.normalizeStreamEvents) {
1372
+ return await failRun('Subagent adapter does not support normalized streaming output');
1373
+ }
1374
+ }
1375
+ const workDir = await ensureSubagentWorkDir(subagentId);
1376
+ if (runPlan.files && runPlan.files.length > 0) {
1377
+ for (const file of runPlan.files) {
1378
+ const filePath = path.join(workDir, file.path);
1379
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
1380
+ await fs.writeFile(filePath, file.contents, 'utf-8');
1381
+ }
1382
+ }
1383
+ const resolvedEnv = runPlan.env
1384
+ ? Object.fromEntries(Object.entries(runPlan.env).map(([key, value]) => {
1385
+ if (typeof value === 'string' && value.includes('{WORKDIR}')) {
1386
+ return [key, value.replace(/\{WORKDIR\}/g, workDir)];
1387
+ }
1388
+ return [key, value];
1389
+ }))
1390
+ : undefined;
1391
+ let runResult = null;
1392
+ let streamResult = null;
1393
+ let activeResult = null;
1394
+ let normalized = null;
1395
+ let attempt = 0;
1396
+ while (attempt < retryConfig.maxAttempts) {
1397
+ attempt += 1;
1398
+ runResult = null;
1399
+ streamResult = null;
1400
+ activeResult = null;
1401
+ normalized = null;
1402
+ let executionError = null;
1403
+ try {
1404
+ if (runPlan.outputFormat === 'stream-json') {
1405
+ streamResult = await runSubagentCommandStreaming(runPlan.command, runPlan.args, {
1406
+ timeoutMs: runPlan.timeoutMs ?? DEFAULT_SUBAGENT_TIMEOUT_MS,
1407
+ cwd: workDir,
1408
+ env: resolvedEnv,
1409
+ onStart: (child) => {
1410
+ activeRun.child = child;
1411
+ if (activeRun.cancelled) {
1412
+ child.kill('SIGTERM');
1413
+ setTimeout(() => {
1414
+ if (!child.killed) {
1415
+ child.kill('SIGKILL');
1416
+ }
1417
+ }, SUBAGENT_CANCEL_KILL_TIMEOUT_MS);
1418
+ }
1419
+ },
1420
+ });
1421
+ }
1422
+ else {
1423
+ runResult = await runSubagentCommand(runPlan.command, runPlan.args, {
1424
+ timeoutMs: runPlan.timeoutMs ?? DEFAULT_SUBAGENT_TIMEOUT_MS,
1425
+ cwd: workDir,
1426
+ env: resolvedEnv,
1427
+ onStart: (child) => {
1428
+ activeRun.child = child;
1429
+ if (activeRun.cancelled) {
1430
+ child.kill('SIGTERM');
1431
+ setTimeout(() => {
1432
+ if (!child.killed) {
1433
+ child.kill('SIGKILL');
1434
+ }
1435
+ }, SUBAGENT_CANCEL_KILL_TIMEOUT_MS);
1436
+ }
1437
+ },
1438
+ });
1439
+ }
1440
+ }
1441
+ catch (error) {
1442
+ executionError = error instanceof Error ? error.message : 'Subagent run failed';
1443
+ }
1444
+ activeResult =
1445
+ streamResult ??
1446
+ runResult ?? {
1447
+ ok: false,
1448
+ stdout: '',
1449
+ stderr: executionError ?? '',
1450
+ exitCode: null,
1451
+ durationMs: 0,
1452
+ timedOut: false,
1453
+ stdoutTruncated: false,
1454
+ stderrTruncated: false,
1455
+ error: executionError ?? 'Subagent run failed',
1456
+ };
1457
+ if (streamResult || runResult) {
1458
+ normalized =
1459
+ runPlan.outputFormat === 'stream-json' && adapter.normalizeStreamingResult
1460
+ ? adapter.normalizeStreamingResult({
1461
+ events: streamResult?.events ?? [],
1462
+ rawOutput: streamResult?.stdout ?? '',
1463
+ })
1464
+ : adapter.normalizeRunResult({
1465
+ stdout: runResult?.stdout ?? '',
1466
+ outputFormat: runPlan.outputFormat === 'stream-json' ? 'json' : runPlan.outputFormat,
1467
+ });
1468
+ }
1469
+ else {
1470
+ normalized = {
1471
+ output: buildErrorOutput(executionError ?? 'Subagent run failed'),
1472
+ metadata: {},
1473
+ parseError: null,
1474
+ };
1475
+ }
1476
+ const parseStrict = runPlan.outputFormat === 'json';
1477
+ const parseError = normalized.parseError ?? null;
1478
+ const retryDecision = classifySubagentRetryable({
1479
+ result: {
1480
+ ok: activeResult.ok,
1481
+ timedOut: activeResult.timedOut,
1482
+ exitCode: activeResult.exitCode,
1483
+ error: activeResult.error,
1484
+ stderr: activeResult.stderr,
1485
+ stdoutTruncated: activeResult.stdoutTruncated,
1486
+ stderrTruncated: activeResult.stderrTruncated,
1487
+ },
1488
+ parseError,
1489
+ parseStrict,
1490
+ config: retryConfig,
1491
+ });
1492
+ if (retryDecision &&
1493
+ attempt < retryConfig.maxAttempts &&
1494
+ !activeRun.cancelled) {
1495
+ retryReasons.push(retryDecision.reason);
1496
+ const delayMs = computeRetryDelayMs(attempt, retryConfig.backoffMs, retryConfig.maxBackoffMs);
1497
+ logInfo('Retrying subagent run', {
1498
+ subagentId,
1499
+ runId: resolvedRunId,
1500
+ attempt,
1501
+ maxAttempts: retryConfig.maxAttempts,
1502
+ reason: retryDecision.reason,
1503
+ delay_ms: delayMs,
1504
+ });
1505
+ await sleep(delayMs);
1506
+ continue;
1507
+ }
1508
+ break;
1509
+ }
1510
+ if (!activeResult || !normalized) {
1511
+ return await failRun('Subagent run failed');
1512
+ }
1513
+ const normalizedResult = normalized;
1514
+ const { data: cancellationRow, error: cancellationError } = await supabase
1515
+ .from('subagent_runs')
1516
+ .select('status')
1517
+ .eq('id', resolvedRunId)
1518
+ .maybeSingle();
1519
+ if (cancellationError) {
1520
+ logError('Failed to check subagent run cancellation', {
1521
+ error: cancellationError.message,
1522
+ subagentId,
1523
+ runId: resolvedRunId,
1524
+ });
1525
+ }
1526
+ const dbCancelled = cancellationRow?.status === 'cancelled';
1527
+ const wasCancelled = activeRun.cancelled || dbCancelled;
1528
+ const cancelReason = activeRun.cancelReason && activeRun.cancelReason.trim().length > 0
1529
+ ? activeRun.cancelReason
1530
+ : 'Cancelled';
1531
+ const output = normalizedResult.output;
1532
+ const parseError = normalizedResult.parseError ?? null;
1533
+ const retryCount = Math.max(0, attempt - 1);
1534
+ const completedAt = new Date().toISOString();
1535
+ const parseStrict = runPlan.outputFormat === 'json';
1536
+ const shouldFail = !activeResult.ok || (parseStrict && !!parseError);
1537
+ const stderrMessage = activeResult.stderr && activeResult.stderr.trim().length > 0
1538
+ ? activeResult.stderr.trim()
1539
+ : null;
1540
+ const exitCodeMessage = activeResult.exitCode === null
1541
+ ? 'Subagent process did not exit cleanly'
1542
+ : `Subagent exited with code ${activeResult.exitCode}`;
1543
+ const errorMessage = !activeResult.ok
1544
+ ? (activeResult.error ?? stderrMessage ?? exitCodeMessage)
1545
+ : parseError ?? null;
1546
+ const failureOutput = buildErrorOutput(wasCancelled ? cancelReason : errorMessage ?? 'Subagent run failed');
1547
+ const metadataUpdate = {
1548
+ schema_version: SUBAGENT_SCHEMA_VERSION,
1549
+ runner: 'gateway',
1550
+ adapter_id: adapter.id,
1551
+ subagent_type: subagentType,
1552
+ run_id: resolvedRunId,
1553
+ run_sequence: runSequence,
1554
+ last_run_at: completedAt,
1555
+ command: `${runPlan.command} ${runPlan.args.join(' ')}`,
1556
+ exit_code: activeResult.exitCode,
1557
+ duration_ms: activeResult.durationMs,
1558
+ timed_out: activeResult.timedOut,
1559
+ stdout_truncated: activeResult.stdoutTruncated,
1560
+ stderr_truncated: activeResult.stderrTruncated,
1561
+ output_format: runPlan.outputFormat,
1562
+ parse_error: parseError,
1563
+ retry_count: retryCount,
1564
+ retry_reasons: retryReasons,
1565
+ retry_max_attempts: retryConfig.maxAttempts,
1566
+ cancel_run_id: null,
1567
+ cancel_requested_at: null,
1568
+ cancel_reason: wasCancelled ? cancelReason : null,
1569
+ cancelled: wasCancelled,
1570
+ };
1571
+ if (activeResult.stderr) {
1572
+ metadataUpdate.stderr = activeResult.stderr;
1573
+ }
1574
+ const mergedMetadata = mergeMetadata(mergeMetadata((subagent.metadata ?? {}), normalized.metadata), metadataUpdate);
1575
+ const runMetadataUpdate = mergeMetadata(mergeMetadata(runMetadata, normalized.metadata), metadataUpdate);
1576
+ if (streamResult?.events?.length && adapter.normalizeStreamEvents) {
1577
+ const normalizedEvents = adapter.normalizeStreamEvents(streamResult.events);
1578
+ const eventRows = normalizedEvents.map((entry, index) => ({
1579
+ run_id: resolvedRunId,
1580
+ subagent_id: subagentId,
1581
+ team_id: subagent.team_id,
1582
+ gateway_id: subagent.gateway_id,
1583
+ sequence: index,
1584
+ event_type: entry.event_type,
1585
+ payload: asJson(trimEventPayload(entry.payload)),
1586
+ }));
1587
+ for (let i = 0; i < eventRows.length; i += SUBAGENT_EVENT_BATCH_SIZE) {
1588
+ const chunk = eventRows.slice(i, i + SUBAGENT_EVENT_BATCH_SIZE);
1589
+ const { error: eventsError } = await supabase
1590
+ .from('subagent_run_events')
1591
+ .insert(chunk);
1592
+ if (eventsError) {
1593
+ logError('Failed to insert subagent run events', {
1594
+ error: eventsError.message,
1595
+ subagentId,
1596
+ runId: resolvedRunId,
1597
+ });
1598
+ }
1599
+ }
1600
+ }
1601
+ await supabase
1602
+ .from('subagent_runs')
1603
+ .update({
1604
+ status: wasCancelled ? 'cancelled' : shouldFail ? 'failed' : 'completed',
1605
+ output: asJson(wasCancelled ? failureOutput : shouldFail ? failureOutput : output),
1606
+ error: wasCancelled ? cancelReason : shouldFail ? errorMessage : null,
1607
+ completed_at: completedAt,
1608
+ metadata: asJson(runMetadataUpdate),
1609
+ })
1610
+ .eq('id', resolvedRunId);
1611
+ const { error: subagentUpdateError } = await supabase
1612
+ .from('subagents')
1613
+ .update({
1614
+ status: wasCancelled ? 'failed' : shouldFail ? 'failed' : 'completed',
1615
+ output: asJson(wasCancelled ? failureOutput : shouldFail ? failureOutput : output),
1616
+ error: wasCancelled ? cancelReason : shouldFail ? errorMessage : null,
1617
+ completed_at: completedAt,
1618
+ metadata: asJson(mergedMetadata),
1619
+ })
1620
+ .eq('id', subagentId);
1621
+ if (subagentUpdateError) {
1622
+ logError('Failed to update subagent after run', {
1623
+ error: subagentUpdateError.message,
1624
+ subagentId,
1625
+ runId: resolvedRunId,
1626
+ });
1627
+ }
1628
+ activeSubagentRuns.delete(subagentId);
1629
+ return {
1630
+ ok: !shouldFail,
1631
+ result: {
1632
+ subagent_id: subagentId,
1633
+ status: shouldFail ? 'failed' : 'completed',
1634
+ output: shouldFail ? null : output,
1635
+ error: shouldFail ? errorMessage : null,
1636
+ },
1637
+ error: shouldFail ? errorMessage ?? 'Subagent run failed' : undefined,
1638
+ };
1639
+ }
1640
+ async function handleModelRunJob(_supabase, job) {
1641
+ const payload = job.payload ?? {};
1642
+ const prompt = typeof payload.prompt === 'string' ? payload.prompt : null;
1643
+ const model = typeof payload.model === 'string' ? payload.model : null;
1644
+ const jsonSchema = payload.json_schema && typeof payload.json_schema === 'object'
1645
+ ? payload.json_schema
1646
+ : null;
1647
+ const appendSystemPrompt = typeof payload.append_system_prompt === 'string' ? payload.append_system_prompt : null;
1648
+ const timeoutMs = typeof payload.timeout_ms === 'number' && payload.timeout_ms > 0
1649
+ ? payload.timeout_ms
1650
+ : DEFAULT_MODEL_RUN_TIMEOUT_MS;
1651
+ if (!prompt || !model) {
1652
+ return {
1653
+ ok: false,
1654
+ result: { message: 'Missing prompt or model for gateway model run' },
1655
+ error: 'Missing prompt or model',
1656
+ };
1657
+ }
1658
+ if (!jsonSchema) {
1659
+ return {
1660
+ ok: false,
1661
+ result: { message: 'Missing json_schema for gateway model run' },
1662
+ error: 'Missing json_schema',
1663
+ };
1664
+ }
1665
+ const providerFromPayload = typeof payload.provider === 'string' ? payload.provider : null;
1666
+ const providerId = (providerFromPayload === 'claude_code' || providerFromPayload === 'codex' || providerFromPayload === 'gemini')
1667
+ ? providerFromPayload
1668
+ : inferGatewayProviderFromModel(model);
1669
+ const provider = getGatewayCliProvider(providerId);
1670
+ let normalizedPrompt = prompt;
1671
+ let normalizedSchema = jsonSchema;
1672
+ if (provider.normalizeSchema) {
1673
+ const normalized = provider.normalizeSchema(jsonSchema);
1674
+ normalizedSchema = normalized.schema;
1675
+ if (normalized.promptSuffix) {
1676
+ normalizedPrompt = `${normalizedPrompt}\n\n${normalized.promptSuffix}`;
1677
+ }
1678
+ }
1679
+ const runPlan = provider.buildModelRunPlan({
1680
+ model,
1681
+ prompt: normalizedPrompt,
1682
+ jsonSchema: normalizedSchema,
1683
+ appendSystemPrompt: appendSystemPrompt ?? undefined,
1684
+ timeoutMs,
1685
+ });
1686
+ const workDir = await ensureSubagentWorkDir(job.id);
1687
+ if (runPlan.files && runPlan.files.length > 0) {
1688
+ for (const file of runPlan.files) {
1689
+ const filePath = path.join(workDir, file.path);
1690
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
1691
+ await fs.writeFile(filePath, file.contents, 'utf-8');
1692
+ }
1693
+ }
1694
+ const resolvedEnv = runPlan.env
1695
+ ? Object.fromEntries(Object.entries(runPlan.env).map(([key, value]) => {
1696
+ if (typeof value === 'string' && value.includes('{WORKDIR}')) {
1697
+ return [key, value.replace(/\{WORKDIR\}/g, workDir)];
1698
+ }
1699
+ return [key, value];
1700
+ }))
1701
+ : undefined;
1702
+ const commandLine = [runPlan.command, ...runPlan.args].join(' ');
1703
+ const debugInfo = {
1704
+ provider_id: providerId,
1705
+ command: runPlan.command,
1706
+ args: runPlan.args,
1707
+ command_line: commandLine,
1708
+ work_dir: workDir,
1709
+ prompt_chars: prompt.length,
1710
+ json_schema_chars: JSON.stringify(normalizedSchema).length,
1711
+ timeout_ms: timeoutMs,
1712
+ output_mode: runPlan.outputMode,
1713
+ output_path: runPlan.outputPath ?? null,
1714
+ };
1715
+ if (runPlan.files && runPlan.files.length > 0) {
1716
+ debugInfo.files = runPlan.files.map((file) => file.path);
1717
+ }
1718
+ await updateJobDebug(_supabase, job.id, debugInfo);
1719
+ const runResult = await runSubagentCommand(runPlan.command, runPlan.args, {
1720
+ timeoutMs: runPlan.timeoutMs ?? DEFAULT_MODEL_RUN_TIMEOUT_MS,
1721
+ cwd: workDir,
1722
+ env: resolvedEnv,
1723
+ });
1724
+ const stdoutInfo = truncateLog(runResult.stdout);
1725
+ const stderrInfo = truncateLog(runResult.stderr);
1726
+ const errorSummary = extractGatewayErrorSummary(stderrInfo.value);
1727
+ debugInfo.stdout = stdoutInfo.value;
1728
+ debugInfo.stdout_truncated = stdoutInfo.truncated;
1729
+ debugInfo.stdout_bytes = stdoutInfo.originalBytes;
1730
+ debugInfo.stderr = stderrInfo.value;
1731
+ debugInfo.stderr_truncated = stderrInfo.truncated;
1732
+ debugInfo.stderr_bytes = stderrInfo.originalBytes;
1733
+ debugInfo.exit_code = runResult.exitCode;
1734
+ debugInfo.duration_ms = runResult.durationMs;
1735
+ debugInfo.timed_out = runResult.timedOut;
1736
+ if (errorSummary) {
1737
+ debugInfo.error_summary = errorSummary;
1738
+ }
1739
+ await updateJobDebug(_supabase, job.id, debugInfo);
1740
+ if (!runResult.ok) {
1741
+ return {
1742
+ ok: false,
1743
+ result: {
1744
+ message: 'Gateway CLI failed for model run',
1745
+ exit_code: runResult.exitCode,
1746
+ stderr: stderrInfo.value,
1747
+ stdout: stdoutInfo.value,
1748
+ command: commandLine,
1749
+ error_summary: errorSummary ?? null,
1750
+ },
1751
+ error: runResult.error ?? 'Gateway model run failed',
1752
+ };
1753
+ }
1754
+ let normalized;
1755
+ try {
1756
+ normalized = provider.normalizeModelRunResult({
1757
+ stdout: runResult.stdout,
1758
+ outputFormat: runPlan.outputFormat,
1759
+ outputMode: runPlan.outputMode,
1760
+ outputPath: runPlan.outputPath,
1761
+ workDir,
1762
+ });
1763
+ }
1764
+ catch (error) {
1765
+ const message = error instanceof Error ? error.message : 'Failed to normalize gateway output';
1766
+ return {
1767
+ ok: false,
1768
+ result: { message, stdout: stdoutInfo.value, stderr: stderrInfo.value, command: commandLine },
1769
+ error: message,
1770
+ };
1771
+ }
1772
+ return {
1773
+ ok: true,
1774
+ result: {
1775
+ output: normalized.output,
1776
+ raw: normalized.raw,
1777
+ usage: normalized.usage,
1778
+ model: normalized.model,
1779
+ duration_ms: runResult.durationMs,
1780
+ exit_code: runResult.exitCode,
1781
+ stdout_truncated: runResult.stdoutTruncated,
1782
+ stderr_truncated: runResult.stderrTruncated,
1783
+ command: commandLine,
1784
+ },
1785
+ };
1786
+ }
1787
+ async function failSubagentRun(supabase, job, errorMessage) {
1788
+ const payload = job.payload ?? {};
1789
+ const subagentId = typeof payload.subagent_id === 'string'
1790
+ ? payload.subagent_id
1791
+ : typeof payload.subagentId === 'string'
1792
+ ? payload.subagentId
1793
+ : null;
1794
+ if (!subagentId)
1795
+ return;
1796
+ const { data: subagent } = await supabase
1797
+ .from('subagents')
1798
+ .select('id, status, subagent_type, metadata, team_id, gateway_id')
1799
+ .eq('id', subagentId)
1800
+ .maybeSingle();
1801
+ if (!subagent)
1802
+ return;
1803
+ if (subagent.status !== 'running')
1804
+ return;
1805
+ const nowIso = new Date().toISOString();
1806
+ const metadataUpdate = {
1807
+ schema_version: SUBAGENT_SCHEMA_VERSION,
1808
+ runner: 'gateway',
1809
+ subagent_type: subagent.subagent_type,
1810
+ last_run_at: nowIso,
1811
+ gateway_error: errorMessage,
1812
+ };
1813
+ const mergedMetadata = mergeMetadata((subagent.metadata ?? {}), metadataUpdate);
1814
+ const failureOutput = buildErrorOutput(errorMessage);
1815
+ const { data: runningRun } = await supabase
1816
+ .from('subagent_runs')
1817
+ .select('id, run_sequence')
1818
+ .eq('subagent_id', subagent.id)
1819
+ .eq('status', 'running')
1820
+ .order('run_sequence', { ascending: false })
1821
+ .limit(1)
1822
+ .maybeSingle();
1823
+ if (runningRun?.id) {
1824
+ const { data: lastEvent } = await supabase
1825
+ .from('subagent_run_events')
1826
+ .select('sequence')
1827
+ .eq('run_id', runningRun.id)
1828
+ .order('sequence', { ascending: false })
1829
+ .limit(1)
1830
+ .maybeSingle();
1831
+ const nextSequence = lastEvent && typeof lastEvent.sequence === 'number' ? lastEvent.sequence + 1 : 0;
1832
+ await supabase
1833
+ .from('subagent_runs')
1834
+ .update({
1835
+ status: 'failed',
1836
+ error: errorMessage,
1837
+ output: asJson(failureOutput),
1838
+ completed_at: nowIso,
1839
+ metadata: asJson(mergedMetadata),
1840
+ })
1841
+ .eq('id', runningRun.id);
1842
+ if (subagent.gateway_id) {
1843
+ await supabase.from('subagent_run_events').insert({
1844
+ run_id: runningRun.id,
1845
+ subagent_id: subagent.id,
1846
+ team_id: subagent.team_id,
1847
+ gateway_id: subagent.gateway_id,
1848
+ sequence: nextSequence,
1849
+ event_type: 'run_failed',
1850
+ payload: asJson({ error: errorMessage }),
1851
+ });
1852
+ }
1853
+ }
1854
+ const { error } = await supabase
1855
+ .from('subagents')
1856
+ .update({
1857
+ status: 'failed',
1858
+ error: errorMessage,
1859
+ completed_at: nowIso,
1860
+ output: asJson(failureOutput),
1861
+ metadata: asJson(mergedMetadata),
1862
+ })
1863
+ .eq('id', subagent.id);
1864
+ if (error) {
1865
+ logError('Failed to mark subagent as failed after gateway error', {
1866
+ subagentId,
1867
+ error: error.message,
1868
+ });
1869
+ }
1870
+ }
1871
+ async function processJob(supabase, config, job) {
1872
+ const claimed = await claimJob(supabase, config, job);
1873
+ if (!claimed) {
1874
+ return;
1875
+ }
1876
+ const jobDetails = describeGatewayJob(claimed);
1877
+ logInfo('Processing gateway job', {
1878
+ jobId: claimed.id,
1879
+ jobType: claimed.job_type,
1880
+ jobKind: jobDetails.job_kind,
1881
+ provider: jobDetails.provider ?? undefined,
1882
+ model: jobDetails.model ?? undefined,
1883
+ subagentType: jobDetails.subagent_type ?? undefined,
1884
+ });
1885
+ if (claimed.job_type === 'diagnostic') {
1886
+ const diagnostic = await handleDiagnosticJob();
1887
+ if (diagnostic.ok) {
1888
+ await completeJob(supabase, claimed.id, 'completed', diagnostic.result);
1889
+ }
1890
+ else {
1891
+ await completeJob(supabase, claimed.id, 'failed', diagnostic.result, diagnostic.error);
1892
+ }
1893
+ return;
1894
+ }
1895
+ if (claimed.job_type === 'subagent_run') {
1896
+ const result = await handleSubagentRunJob(supabase, claimed);
1897
+ if (result.ok) {
1898
+ await completeJob(supabase, claimed.id, 'completed', result.result);
1899
+ }
1900
+ else {
1901
+ await completeJob(supabase, claimed.id, 'failed', result.result, result.error);
1902
+ }
1903
+ return;
1904
+ }
1905
+ if (claimed.job_type === 'model_run') {
1906
+ const result = await handleModelRunJob(supabase, claimed);
1907
+ if (result.ok) {
1908
+ await completeJob(supabase, claimed.id, 'completed', result.result);
1909
+ }
1910
+ else {
1911
+ await completeJob(supabase, claimed.id, 'failed', result.result, result.error);
1912
+ }
1913
+ return;
1914
+ }
1915
+ if (claimed.job_type === 'subagent_cancel') {
1916
+ const result = await handleSubagentCancelJob(supabase, claimed);
1917
+ if (result.ok) {
1918
+ await completeJob(supabase, claimed.id, 'completed', result.result);
1919
+ }
1920
+ else {
1921
+ await completeJob(supabase, claimed.id, 'failed', result.result, result.error);
1922
+ }
1923
+ return;
1924
+ }
1925
+ await completeJob(supabase, claimed.id, 'failed', {
1926
+ message: 'Unsupported job type',
1927
+ job_type: claimed.job_type,
1928
+ }, `Unsupported job type: ${claimed.job_type}`);
1929
+ }
1930
+ async function startGateway(options) {
1931
+ const daemonRequested = options.daemon === true || options.background === true;
1932
+ const foregroundRequested = options.foreground === true;
1933
+ if (daemonRequested && !foregroundRequested) {
1934
+ const existingPid = readPidFile();
1935
+ if (existingPid && isProcessAlive(existingPid)) {
1936
+ logInfo('Gateway already running', { pid: existingPid });
1937
+ return;
1938
+ }
1939
+ const entryPath = process.argv[1] || '';
1940
+ if (entryPath.endsWith('.ts')) {
1941
+ throw new Error('Daemon mode requires the built gateway. Run "pnpm gateway:start -- --daemon".');
1942
+ }
1943
+ await fs.mkdir(path.dirname(LOG_PATH), { recursive: true });
1944
+ const logHandle = await fs.open(LOG_PATH, 'a');
1945
+ const childArgs = [entryPath, 'start', '--foreground'];
1946
+ const deviceName = getStringOption(options, 'device-name') || process.env.PANORAMA_GATEWAY_DEVICE_NAME;
1947
+ if (deviceName) {
1948
+ childArgs.push('--device-name', deviceName);
1949
+ }
1950
+ const child = spawn(process.execPath, childArgs, {
1951
+ detached: true,
1952
+ stdio: ['ignore', logHandle.fd, logHandle.fd],
1953
+ env: {
1954
+ ...process.env,
1955
+ PANORAMA_GATEWAY_LOG_PATH: LOG_PATH,
1956
+ PANORAMA_GATEWAY_PID_PATH: PID_PATH,
1957
+ },
1958
+ });
1959
+ child.unref();
1960
+ await logHandle.close();
1961
+ if (!child.pid) {
1962
+ throw new Error('Failed to determine gateway process id');
1963
+ }
1964
+ await writePidFile(child.pid);
1965
+ logInfo('Gateway started in background', { pid: child.pid, logPath: LOG_PATH });
1966
+ return;
1967
+ }
1968
+ let config;
1969
+ try {
1970
+ config = await loadConfig();
1971
+ }
1972
+ catch (error) {
1973
+ throw new Error(`Gateway config not found. Run "pair" first or set PANORAMA_GATEWAY_CONFIG_PATH. (${String(error)})`);
1974
+ }
1975
+ const supabaseUrl = resolveSupabaseUrl(options, config);
1976
+ const supabaseAnonKey = resolveSupabaseAnonKey(options, config);
1977
+ if (!supabaseUrl || !supabaseAnonKey) {
1978
+ throw new Error('Missing Supabase URL or anon key for gateway start');
1979
+ }
1980
+ const deviceName = getStringOption(options, 'device-name') ||
1981
+ config.deviceName ||
1982
+ process.env.PANORAMA_GATEWAY_DEVICE_NAME ||
1983
+ os.hostname();
1984
+ config = {
1985
+ ...config,
1986
+ supabaseUrl,
1987
+ supabaseAnonKey,
1988
+ deviceName,
1989
+ };
1990
+ await saveConfig(config);
1991
+ const supabase = createClient(supabaseUrl, supabaseAnonKey, {
1992
+ auth: {
1993
+ persistSession: false,
1994
+ autoRefreshToken: true,
1995
+ detectSessionInUrl: false,
1996
+ },
1997
+ realtime: {
1998
+ params: {
1999
+ eventsPerSecond: 10,
2000
+ },
2001
+ },
2002
+ });
2003
+ const { data: sessionData, error: sessionError } = await supabase.auth.setSession({
2004
+ access_token: config.accessToken,
2005
+ refresh_token: config.refreshToken,
2006
+ });
2007
+ if (sessionError || !sessionData?.session) {
2008
+ throw new Error(`Failed to authenticate gateway session: ${sessionError?.message}`);
2009
+ }
2010
+ config.accessToken = sessionData.session.access_token;
2011
+ config.refreshToken = sessionData.session.refresh_token;
2012
+ await saveConfig(config);
2013
+ supabase.auth.onAuthStateChange((event, session) => {
2014
+ if (!session)
2015
+ return;
2016
+ if (event === 'TOKEN_REFRESHED' || event === 'SIGNED_IN') {
2017
+ config.accessToken = session.access_token;
2018
+ config.refreshToken = session.refresh_token;
2019
+ void saveConfig(config)
2020
+ .then(() => logInfo('Gateway session refreshed'))
2021
+ .catch((error) => {
2022
+ logError('Failed to persist refreshed gateway session', {
2023
+ error: error instanceof Error ? error.message : String(error),
2024
+ });
2025
+ });
2026
+ }
2027
+ });
2028
+ await ensureClaudeGatewayHome();
2029
+ const capabilities = await buildCapabilities();
2030
+ const providerStatus = (capabilities.providers ?? {});
2031
+ const anyProviderReady = Object.values(providerStatus).some((entry) => entry?.available === true);
2032
+ const initialStatus = anyProviderReady ? 'ready' : 'error';
2033
+ logInfo('Gateway capabilities loaded', {
2034
+ providers: Object.fromEntries(Object.entries(providerStatus).map(([id, entry]) => [
2035
+ id,
2036
+ {
2037
+ available: entry.available,
2038
+ version: entry.version ?? null,
2039
+ },
2040
+ ])),
2041
+ concurrency: GATEWAY_CONCURRENCY,
2042
+ });
2043
+ await sendHeartbeat(supabase, initialStatus, capabilities, deviceName);
2044
+ let heartbeatStatus = initialStatus;
2045
+ await writePidFile(process.pid);
2046
+ const heartbeatTimer = setInterval(() => {
2047
+ void sendHeartbeat(supabase, heartbeatStatus, capabilities, deviceName);
2048
+ }, DEFAULT_HEARTBEAT_INTERVAL_MS);
2049
+ const pendingJobs = [];
2050
+ const queuedJobIds = new Set();
2051
+ const activeJobIds = new Set();
2052
+ const processCancelJob = async (job) => {
2053
+ const claimed = await claimJob(supabase, config, job);
2054
+ if (!claimed)
2055
+ return;
2056
+ const result = await handleSubagentCancelJob(supabase, claimed);
2057
+ if (result.ok) {
2058
+ await completeJob(supabase, claimed.id, 'completed', result.result);
2059
+ }
2060
+ else {
2061
+ await completeJob(supabase, claimed.id, 'failed', result.result, result.error);
2062
+ }
2063
+ };
2064
+ const processJobWithHandling = async (nextJob) => {
2065
+ try {
2066
+ await processJob(supabase, config, nextJob);
2067
+ }
2068
+ catch (error) {
2069
+ const errorMessage = error instanceof Error ? error.message : String(error);
2070
+ logError('Gateway job processing failed', {
2071
+ jobId: nextJob.id,
2072
+ error: errorMessage,
2073
+ });
2074
+ await completeJob(supabase, nextJob.id, 'failed', { message: 'Gateway job processing failed', job_type: nextJob.job_type }, errorMessage);
2075
+ if (nextJob.job_type === 'subagent_run') {
2076
+ await failSubagentRun(supabase, nextJob, errorMessage);
2077
+ }
2078
+ }
2079
+ finally {
2080
+ activeJobIds.delete(nextJob.id);
2081
+ void processQueue();
2082
+ }
2083
+ };
2084
+ const processQueue = async () => {
2085
+ while (activeJobIds.size < GATEWAY_CONCURRENCY && pendingJobs.length > 0) {
2086
+ const nextJob = pendingJobs.shift();
2087
+ if (!nextJob)
2088
+ break;
2089
+ queuedJobIds.delete(nextJob.id);
2090
+ if (activeJobIds.has(nextJob.id))
2091
+ continue;
2092
+ activeJobIds.add(nextJob.id);
2093
+ void processJobWithHandling(nextJob);
2094
+ }
2095
+ };
2096
+ const enqueueJob = (job) => {
2097
+ if (!isJobTargeted(job, config.gatewayId))
2098
+ return;
2099
+ if (job.job_type === 'subagent_cancel') {
2100
+ void processCancelJob(job);
2101
+ return;
2102
+ }
2103
+ if (queuedJobIds.has(job.id) || activeJobIds.has(job.id)) {
2104
+ return;
2105
+ }
2106
+ pendingJobs.push(job);
2107
+ queuedJobIds.add(job.id);
2108
+ void processQueue();
2109
+ };
2110
+ const { data: backlog, error: backlogError } = await supabase
2111
+ .from('gateway_jobs')
2112
+ .select('*')
2113
+ .eq('team_id', config.teamId)
2114
+ .eq('status', 'queued')
2115
+ .or(`gateway_id.is.null,gateway_id.eq.${config.gatewayId}`)
2116
+ .order('created_at', { ascending: true });
2117
+ if (backlogError) {
2118
+ logError('Failed to fetch queued gateway jobs', { error: backlogError.message });
2119
+ }
2120
+ else if (backlog && backlog.length > 0) {
2121
+ backlog.forEach((job) => enqueueJob(job));
2122
+ }
2123
+ const channel = supabase
2124
+ .channel(`gateway_jobs_${config.gatewayId}`)
2125
+ .on('postgres_changes', {
2126
+ event: 'INSERT',
2127
+ schema: 'public',
2128
+ table: 'gateway_jobs',
2129
+ filter: `team_id=eq.${config.teamId}`,
2130
+ }, (payload) => {
2131
+ const job = payload.new;
2132
+ if (job.status !== 'queued')
2133
+ return;
2134
+ enqueueJob(job);
2135
+ })
2136
+ .subscribe((status) => {
2137
+ if (status === 'SUBSCRIBED') {
2138
+ logInfo('Gateway subscribed to job queue');
2139
+ }
2140
+ if (status === 'CHANNEL_ERROR') {
2141
+ logError('Gateway realtime channel error');
2142
+ heartbeatStatus = 'error';
2143
+ }
2144
+ });
2145
+ const shutdown = async (signal) => {
2146
+ logInfo('Gateway shutting down', { signal });
2147
+ clearInterval(heartbeatTimer);
2148
+ heartbeatStatus = 'offline';
2149
+ await sendHeartbeat(supabase, 'offline', capabilities, deviceName);
2150
+ await channel.unsubscribe();
2151
+ await removePidFile();
2152
+ process.exit(0);
2153
+ };
2154
+ process.on('SIGINT', () => void shutdown('SIGINT'));
2155
+ process.on('SIGTERM', () => void shutdown('SIGTERM'));
2156
+ logInfo('Gateway running', {
2157
+ gatewayId: config.gatewayId,
2158
+ teamId: config.teamId,
2159
+ deviceName,
2160
+ heartbeatIntervalMs: DEFAULT_HEARTBEAT_INTERVAL_MS,
2161
+ concurrency: GATEWAY_CONCURRENCY,
2162
+ });
2163
+ }
2164
+ async function stopGateway() {
2165
+ const pid = readPidFile();
2166
+ if (!pid) {
2167
+ logInfo('No gateway pid file found', { pidPath: PID_PATH });
2168
+ return;
2169
+ }
2170
+ if (!isProcessAlive(pid)) {
2171
+ logInfo('Gateway not running, removing stale pid file', { pid });
2172
+ await removePidFile();
2173
+ return;
2174
+ }
2175
+ try {
2176
+ process.kill(pid, 'SIGTERM');
2177
+ logInfo('Sent SIGTERM to gateway process', { pid });
2178
+ }
2179
+ catch (error) {
2180
+ logError('Failed to stop gateway process', {
2181
+ pid,
2182
+ error: error instanceof Error ? error.message : String(error),
2183
+ });
2184
+ return;
2185
+ }
2186
+ const timeoutMs = 5000;
2187
+ const start = Date.now();
2188
+ while (Date.now() - start < timeoutMs) {
2189
+ if (!isProcessAlive(pid)) {
2190
+ await removePidFile();
2191
+ logInfo('Gateway stopped', { pid });
2192
+ return;
2193
+ }
2194
+ await new Promise((resolve) => setTimeout(resolve, 200));
2195
+ }
2196
+ logInfo('Gateway stop timed out; process may still be running', { pid });
2197
+ }
2198
+ async function showLogs(options) {
2199
+ const linesRaw = getStringOption(options, 'lines');
2200
+ const lines = linesRaw ? Number.parseInt(linesRaw, 10) : 200;
2201
+ const follow = options['no-follow'] !== true;
2202
+ try {
2203
+ const content = await fs.readFile(LOG_PATH, 'utf-8');
2204
+ const allLines = content.split(/\r?\n/);
2205
+ const tail = Number.isFinite(lines) && lines > 0 ? allLines.slice(-lines) : allLines;
2206
+ process.stdout.write(`${tail.join('\n')}\n`);
2207
+ }
2208
+ catch (error) {
2209
+ logError('Unable to read gateway log file', {
2210
+ logPath: LOG_PATH,
2211
+ error: error instanceof Error ? error.message : String(error),
2212
+ });
2213
+ return;
2214
+ }
2215
+ if (!follow)
2216
+ return;
2217
+ let position = 0;
2218
+ try {
2219
+ const stat = await fs.stat(LOG_PATH);
2220
+ position = stat.size;
2221
+ }
2222
+ catch {
2223
+ position = 0;
2224
+ }
2225
+ logInfo('Tailing gateway logs', { logPath: LOG_PATH });
2226
+ fsSync.watch(LOG_PATH, { persistent: true }, async (event) => {
2227
+ if (event !== 'change')
2228
+ return;
2229
+ try {
2230
+ const stat = await fs.stat(LOG_PATH);
2231
+ if (stat.size < position) {
2232
+ position = 0;
2233
+ }
2234
+ const handle = await fs.open(LOG_PATH, 'r');
2235
+ const length = stat.size - position;
2236
+ if (length > 0) {
2237
+ const buffer = Buffer.alloc(length);
2238
+ await handle.read(buffer, 0, length, position);
2239
+ position = stat.size;
2240
+ process.stdout.write(buffer.toString('utf-8'));
2241
+ }
2242
+ await handle.close();
2243
+ }
2244
+ catch (error) {
2245
+ logError('Failed to tail gateway logs', {
2246
+ logPath: LOG_PATH,
2247
+ error: error instanceof Error ? error.message : String(error),
2248
+ });
2249
+ }
2250
+ });
2251
+ await new Promise(() => undefined);
2252
+ }
2253
+ async function run() {
2254
+ const parsed = parseArgs(process.argv.slice(2));
2255
+ if (parsed.options.h || parsed.options.help || parsed.command === null) {
2256
+ printHelp();
2257
+ return;
2258
+ }
2259
+ if (parsed.command === 'pair') {
2260
+ loadEnvironment(parsed.options);
2261
+ const code = getStringOption(parsed.options, 'code') || parsed.positional[0];
2262
+ if (!code) {
2263
+ throw new Error('Pairing code is required');
2264
+ }
2265
+ await pairGateway(code, parsed.options);
2266
+ return;
2267
+ }
2268
+ if (parsed.command === 'start') {
2269
+ loadEnvironment(parsed.options);
2270
+ await startGateway(parsed.options);
2271
+ return;
2272
+ }
2273
+ if (parsed.command === 'stop') {
2274
+ await stopGateway();
2275
+ return;
2276
+ }
2277
+ if (parsed.command === 'logs') {
2278
+ await showLogs(parsed.options);
2279
+ return;
2280
+ }
2281
+ printHelp();
2282
+ process.exitCode = 1;
2283
+ }
2284
+ run().catch((error) => {
2285
+ logError('Gateway failed', { error: error instanceof Error ? error.message : String(error) });
2286
+ process.exitCode = 1;
2287
+ });
2288
+ //# sourceMappingURL=index.js.map