agent-embassy 1.7.1 → 1.8.1

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 (121) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/CONTRIBUTING.md +51 -47
  3. package/README.md +26 -9
  4. package/README.zh-CN.md +22 -9
  5. package/SECURITY.md +48 -35
  6. package/dist/src/gateway/acp-client.d.ts +12 -19
  7. package/dist/src/gateway/acp-client.js +90 -65
  8. package/dist/src/gateway/acp-client.js.map +1 -1
  9. package/dist/src/gateway/acp-provider.d.ts +11 -10
  10. package/dist/src/gateway/acp-provider.js +165 -53
  11. package/dist/src/gateway/acp-provider.js.map +1 -1
  12. package/dist/src/gateway/claude-helper-client.d.ts +2 -43
  13. package/dist/src/gateway/claude-helper-client.js +1 -211
  14. package/dist/src/gateway/claude-helper-client.js.map +1 -1
  15. package/dist/src/gateway/claude-helper-protocol.d.ts +16 -74
  16. package/dist/src/gateway/claude-helper-protocol.js +99 -378
  17. package/dist/src/gateway/claude-helper-protocol.js.map +1 -1
  18. package/dist/src/gateway/claude-helper-supervisor.d.ts +56 -44
  19. package/dist/src/gateway/claude-helper-supervisor.js +265 -456
  20. package/dist/src/gateway/claude-helper-supervisor.js.map +1 -1
  21. package/dist/src/gateway/claude-helper.js +184 -210
  22. package/dist/src/gateway/claude-helper.js.map +1 -1
  23. package/dist/src/gateway/claude-peer.d.ts +20 -87
  24. package/dist/src/gateway/claude-peer.js +310 -1009
  25. package/dist/src/gateway/claude-peer.js.map +1 -1
  26. package/dist/src/gateway/claude-runtime.d.ts +13 -15
  27. package/dist/src/gateway/claude-runtime.js +14 -168
  28. package/dist/src/gateway/claude-runtime.js.map +1 -1
  29. package/dist/src/gateway/cli-copy.d.ts +1 -1
  30. package/dist/src/gateway/cli-copy.en.d.ts +2 -4
  31. package/dist/src/gateway/cli-copy.en.js +3 -4
  32. package/dist/src/gateway/cli-copy.en.js.map +1 -1
  33. package/dist/src/gateway/cli-copy.js +0 -2
  34. package/dist/src/gateway/cli-copy.js.map +1 -1
  35. package/dist/src/gateway/cli-copy.zh-CN.d.ts +2 -4
  36. package/dist/src/gateway/cli-copy.zh-CN.js +3 -4
  37. package/dist/src/gateway/cli-copy.zh-CN.js.map +1 -1
  38. package/dist/src/gateway/cli.d.ts +7 -2
  39. package/dist/src/gateway/cli.js +273 -617
  40. package/dist/src/gateway/cli.js.map +1 -1
  41. package/dist/src/gateway/codex-app-server.d.ts +5 -227
  42. package/dist/src/gateway/codex-app-server.js +18 -1463
  43. package/dist/src/gateway/codex-app-server.js.map +1 -1
  44. package/dist/src/gateway/codex-doctor.d.ts +6 -3
  45. package/dist/src/gateway/codex-doctor.js +77 -98
  46. package/dist/src/gateway/codex-doctor.js.map +1 -1
  47. package/dist/src/gateway/codex-local-transport.d.ts +1 -6
  48. package/dist/src/gateway/codex-local-transport.js +8 -10
  49. package/dist/src/gateway/codex-local-transport.js.map +1 -1
  50. package/dist/src/gateway/codex-stateless-transport.d.ts +114 -0
  51. package/dist/src/gateway/codex-stateless-transport.js +1108 -0
  52. package/dist/src/gateway/codex-stateless-transport.js.map +1 -0
  53. package/dist/src/gateway/config.d.ts +2 -9
  54. package/dist/src/gateway/config.js +62 -100
  55. package/dist/src/gateway/config.js.map +1 -1
  56. package/dist/src/gateway/control.d.ts +36 -90
  57. package/dist/src/gateway/control.js +464 -1159
  58. package/dist/src/gateway/control.js.map +1 -1
  59. package/dist/src/gateway/dashboard-copy.d.ts +1 -1
  60. package/dist/src/gateway/dashboard-copy.en.d.ts +3 -26
  61. package/dist/src/gateway/dashboard-copy.en.js +14 -37
  62. package/dist/src/gateway/dashboard-copy.en.js.map +1 -1
  63. package/dist/src/gateway/dashboard-copy.js +3 -26
  64. package/dist/src/gateway/dashboard-copy.js.map +1 -1
  65. package/dist/src/gateway/dashboard-copy.zh-CN.d.ts +3 -26
  66. package/dist/src/gateway/dashboard-copy.zh-CN.js +14 -37
  67. package/dist/src/gateway/dashboard-copy.zh-CN.js.map +1 -1
  68. package/dist/src/gateway/dashboard-model.d.ts +6 -23
  69. package/dist/src/gateway/dashboard-model.js +37 -131
  70. package/dist/src/gateway/dashboard-model.js.map +1 -1
  71. package/dist/src/gateway/dashboard.d.ts +0 -6
  72. package/dist/src/gateway/dashboard.js +0 -6
  73. package/dist/src/gateway/dashboard.js.map +1 -1
  74. package/dist/src/gateway/live-dashboard-app/app.js +9 -32
  75. package/dist/src/gateway/live-dashboard-command.js +2 -2
  76. package/dist/src/gateway/live-dashboard-command.js.map +1 -1
  77. package/dist/src/gateway/live-dashboard-http.d.ts +1 -1
  78. package/dist/src/gateway/live-dashboard-http.js +2 -2
  79. package/dist/src/gateway/live-dashboard-http.js.map +1 -1
  80. package/dist/src/gateway/progress-watch-machine.d.ts +1 -37
  81. package/dist/src/gateway/progress-watch-machine.js +4 -15
  82. package/dist/src/gateway/progress-watch-machine.js.map +1 -1
  83. package/dist/src/gateway/providers.d.ts +55 -195
  84. package/dist/src/gateway/providers.js +608 -2465
  85. package/dist/src/gateway/providers.js.map +1 -1
  86. package/dist/src/gateway/server.d.ts +16 -25
  87. package/dist/src/gateway/server.js +164 -251
  88. package/dist/src/gateway/server.js.map +1 -1
  89. package/dist/src/gateway/service.d.ts +146 -448
  90. package/dist/src/gateway/service.js +1670 -6142
  91. package/dist/src/gateway/service.js.map +1 -1
  92. package/dist/src/gateway/state-v2-to-v3.d.ts +23 -0
  93. package/dist/src/gateway/state-v2-to-v3.js +994 -0
  94. package/dist/src/gateway/state-v2-to-v3.js.map +1 -0
  95. package/dist/src/gateway/store.d.ts +54 -309
  96. package/dist/src/gateway/store.js +1563 -3519
  97. package/dist/src/gateway/store.js.map +1 -1
  98. package/dist/src/gateway/types.d.ts +175 -228
  99. package/dist/src/gateway/types.js +114 -252
  100. package/dist/src/gateway/types.js.map +1 -1
  101. package/docs/CONFIGURATION.md +22 -9
  102. package/docs/CONFIGURATION.zh-CN.md +21 -9
  103. package/docs/DASHBOARD.md +10 -10
  104. package/docs/DASHBOARD.zh-CN.md +3 -3
  105. package/docs/DELIVERY.md +4 -4
  106. package/docs/DELIVERY.zh-CN.md +4 -4
  107. package/docs/GATEWAY-ARCHITECTURE.md +158 -187
  108. package/package.json +1 -1
  109. package/skills/embassy-peer/SKILL.md +28 -22
  110. package/dist/src/gateway/codex-registration-generation.d.ts +0 -5
  111. package/dist/src/gateway/codex-registration-generation.js +0 -19
  112. package/dist/src/gateway/codex-registration-generation.js.map +0 -1
  113. package/dist/src/gateway/codex-registration-succession.d.ts +0 -209
  114. package/dist/src/gateway/codex-registration-succession.js +0 -588
  115. package/dist/src/gateway/codex-registration-succession.js.map +0 -1
  116. package/dist/src/gateway/compatibility.d.ts +0 -22
  117. package/dist/src/gateway/compatibility.js +0 -23
  118. package/dist/src/gateway/compatibility.js.map +0 -1
  119. package/dist/src/gateway/delivery-machine.d.ts +0 -235
  120. package/dist/src/gateway/delivery-machine.js +0 -540
  121. package/dist/src/gateway/delivery-machine.js.map +0 -1
@@ -1,55 +1,35 @@
1
1
  #!/usr/bin/env node
2
- /**
3
- * Local gateway foreground launcher and metadata-only control client.
4
- *
5
- * Provider identities and message bodies are accepted only from inherited
6
- * process state and stdin respectively. They are never reflected in output or
7
- * error text. Only `serve` owns the long-lived provider capabilities and
8
- * private control server; every other command is a bounded client operation.
9
- */
2
+ /** Foreground broker plus bounded metadata-only control client. */
10
3
  import { realpathSync } from "node:fs";
11
4
  import { lstat, realpath } from "node:fs/promises";
12
5
  import path from "node:path";
13
6
  import { fileURLToPath } from "node:url";
14
7
  import { BridgeError } from "../errors.js";
15
- import { getCliCopy, } from "./cli-copy.js";
8
+ import { getCliCopy } from "./cli-copy.js";
16
9
  import { callerIdentityConflictHintEn } from "./cli-copy.en.js";
17
10
  import { callerIdentityConflictHintZhCn } from "./cli-copy.zh-CN.js";
18
- import { GATEWAY_CONTROL_DEFAULT_TIMEOUT_MS, GATEWAY_CONTROL_MAX_MESSAGE_BYTES, GATEWAY_CONTROL_MAX_RESPONSE_BYTES, GATEWAY_CONTROL_PROTOCOL_VERSION, GatewayControlTransportError, isClaudeSessionSelector, isGatewayAlias, isGatewayConversationId, isGatewayDeliveryToken, isGatewayHostId, isGatewayReplyAddress, sendGatewayControlRequest, } from "./control.js";
11
+ import { GATEWAY_CONTROL_DEFAULT_TIMEOUT_MS, GATEWAY_CONTROL_MAX_MESSAGE_BYTES, GATEWAY_CONTROL_MAX_RESPONSE_BYTES, GATEWAY_CONTROL_PROTOCOL_VERSION, GatewayControlTransportError, isClaudeSessionSelector, isGatewayAlias, isGatewayConversationId, isGatewayDeliveryToken, isGatewayHostId, isGatewayReplyAddress, sendGatewayControlRequest } from "./control.js";
19
12
  import { loadGatewayConfig } from "./config.js";
20
- import { isDashboardLocale, } from "./locale.js";
21
- import { DEFAULT_LIVE_DASHBOARD_PORT, runLiveDashboardCommand, } from "./live-dashboard-command.js";
22
- import { runGatewayServer, } from "./server.js";
13
+ import { isDashboardLocale } from "./locale.js";
14
+ import { DEFAULT_LIVE_DASHBOARD_PORT, runLiveDashboardCommand } from "./live-dashboard-command.js";
15
+ import { runGatewayServer } from "./server.js";
23
16
  import { PROGRESS_WATCH_DEFAULT_IDLE_MS } from "./progress-watch-machine.js";
17
+ import { convertGatewayStateV2ToV3 } from "./state-v2-to-v3.js";
24
18
  const THREAD_ID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
25
19
  const DEFAULT_HOST_ID = "this-mac";
26
20
  const CLI_MAX_OUTPUT_BYTES = GATEWAY_CONTROL_MAX_RESPONSE_BYTES;
27
21
  const DELIVERY_POLL_INTERVAL_MS = 250;
28
22
  const DELIVERY_POLL_MIN_REQUEST_TIMEOUT_MS = 50;
29
- export const EMBASSY_VERSION = "1.7.1";
23
+ export const EMBASSY_VERSION = "1.8.1";
30
24
  // RELEASE VERSION SWEEP — every place the version lives: package.json,
31
- // npm-shrinkwrap.json (x2), this constant, codex-app-server.ts clientInfo,
25
+ // npm-shrinkwrap.json (x2), this constant,
32
26
  // test/gateway-cli.test.ts package-metadata assertion.
33
27
  const DEFAULT_CLI_LOCALE = "en";
34
28
  export const gatewayCliCommands = [
35
- "serve",
36
- "health",
37
- "status",
38
- "doctor",
39
- "delivery-status",
40
- "wait-delivery",
41
- "untrack",
42
- "refresh-dashboard",
43
- "dashboard",
44
- "register-codex",
45
- "unregister-codex",
46
- "select-claude",
47
- "unselect-claude",
48
- "pair",
49
- "unpair",
50
- "send-to-claude",
51
- "send-to-codex",
52
- "reply",
29
+ "serve", "health", "status", "doctor", "convert-state-v2-to-v3", "delivery-status",
30
+ "wait-delivery", "untrack", "refresh-dashboard", "dashboard", "register-codex",
31
+ "unregister-codex", "select-claude", "unselect-claude", "pair", "unpair",
32
+ "send-to-claude", "send-to-codex", "reply",
53
33
  ];
54
34
  export const gatewayCliExitCodes = Object.freeze({
55
35
  ok: 0,
@@ -66,58 +46,42 @@ class CliFault extends Error {
66
46
  kind;
67
47
  constructor(code, retryable = false, hint, kind) {
68
48
  super("The gateway client rejected the request.");
69
- this.name = "CliFault";
70
49
  this.code = code;
71
50
  this.retryable = retryable;
72
51
  this.hint = hint;
73
52
  this.kind = kind;
53
+ this.name = "CliFault";
74
54
  }
75
55
  }
76
- function isCommand(value) {
77
- return (value !== undefined &&
78
- gatewayCliCommands.includes(value));
79
- }
56
+ function fault(code = "INVALID_ARGUMENTS") { throw new CliFault(code); }
57
+ const isCommand = (value) => value !== undefined && gatewayCliCommands.includes(value);
80
58
  function parseOptions(args, valueNames, flagNames = []) {
81
- const values = new Set(valueNames);
82
- const flags = new Set(flagNames);
59
+ const values = new Set(valueNames), flags = new Set(flagNames);
83
60
  const parsed = Object.create(null);
84
61
  for (let index = 0; index < args.length; index += 1) {
85
62
  const token = args[index];
86
- if (token === undefined || !token.startsWith("--") || token.length <= 2) {
87
- throw new CliFault("INVALID_ARGUMENTS");
88
- }
63
+ if (token === undefined || !token.startsWith("--") || token.length <= 2)
64
+ fault();
89
65
  const name = token.slice(2);
90
- if (Object.hasOwn(parsed, name)) {
91
- throw new CliFault("INVALID_ARGUMENTS");
92
- }
66
+ if (Object.hasOwn(parsed, name))
67
+ fault();
93
68
  if (flags.has(name)) {
94
69
  parsed[name] = true;
95
70
  continue;
96
71
  }
97
- if (!values.has(name)) {
98
- throw new CliFault("INVALID_ARGUMENTS");
99
- }
72
+ if (!values.has(name))
73
+ fault();
100
74
  const value = args[index + 1];
101
- if (value === undefined || value.startsWith("--") || value.length === 0) {
102
- throw new CliFault("INVALID_ARGUMENTS");
103
- }
75
+ if (value === undefined || value.startsWith("--") || value.length === 0)
76
+ fault();
104
77
  parsed[name] = value;
105
78
  index += 1;
106
79
  }
107
80
  return parsed;
108
81
  }
109
- function environmentLocale(env) {
110
- const value = env.EMBASSY_LOCALE;
111
- if (value === undefined || value.length === 0)
112
- return DEFAULT_CLI_LOCALE;
113
- if (!isDashboardLocale(value))
114
- throw new CliFault("INVALID_ARGUMENTS");
115
- return value;
116
- }
117
- function resolveCommonCliOptions(args, env) {
82
+ function commonOptions(args, env) {
118
83
  const stripped = [];
119
84
  let locale;
120
- let sawLocale = false;
121
85
  for (let index = 0; index < args.length; index += 1) {
122
86
  const token = args[index];
123
87
  if (token !== "--lang") {
@@ -125,189 +89,144 @@ function resolveCommonCliOptions(args, env) {
125
89
  stripped.push(token);
126
90
  continue;
127
91
  }
128
- if (sawLocale)
129
- throw new CliFault("INVALID_ARGUMENTS");
130
- sawLocale = true;
92
+ if (locale !== undefined)
93
+ fault();
131
94
  const value = args[index + 1];
132
- if (value === undefined || value.startsWith("--") || value.length === 0) {
133
- throw new CliFault("INVALID_ARGUMENTS");
134
- }
135
- if (!isDashboardLocale(value)) {
136
- throw new CliFault("INVALID_ARGUMENTS");
137
- }
95
+ if (!isDashboardLocale(value))
96
+ fault();
138
97
  locale = value;
139
98
  index += 1;
140
99
  }
100
+ const inherited = env.EMBASSY_LOCALE;
101
+ if (locale === undefined && inherited !== undefined && inherited.length > 0 && !isDashboardLocale(inherited))
102
+ fault();
141
103
  return {
142
104
  args: stripped,
143
- // Resolve the environment only when no valid explicit flag exists. This
144
- // lets an exact flag intentionally override even a malformed environment.
145
- locale: locale ?? environmentLocale(env),
105
+ locale: locale ?? (isDashboardLocale(inherited) ? inherited : DEFAULT_CLI_LOCALE),
146
106
  };
147
107
  }
148
108
  function fallbackCliLocale(args, env) {
149
- const localeFlagIndices = [];
150
- for (let index = 0; index < args.length; index += 1) {
151
- if (args[index] === "--lang")
152
- localeFlagIndices.push(index);
153
- }
154
- if (localeFlagIndices.length === 1) {
155
- const value = args[(localeFlagIndices[0] ?? -1) + 1];
156
- if (isDashboardLocale(value))
157
- return value;
158
- }
159
- return isDashboardLocale(env.EMBASSY_LOCALE)
160
- ? env.EMBASSY_LOCALE
161
- : DEFAULT_CLI_LOCALE;
162
- }
163
- function fixedStderr(locale, kind) {
164
- return `[embassy] ${getCliCopy(locale)[`error.${kind}`]}\n`;
109
+ const indices = args.flatMap((value, index) => value === "--lang" ? [index] : []);
110
+ const flagged = indices.length === 1 ? args[indices[0] + 1] : undefined;
111
+ return isDashboardLocale(flagged) ? flagged
112
+ : isDashboardLocale(env.EMBASSY_LOCALE) ? env.EMBASSY_LOCALE : DEFAULT_CLI_LOCALE;
165
113
  }
114
+ const fixedStderr = (locale, kind) => `[embassy] ${getCliCopy(locale)[`error.${kind}`]}\n`;
166
115
  function requireString(options, name) {
167
116
  const value = options[name];
168
117
  if (typeof value !== "string")
169
- throw new CliFault("INVALID_ARGUMENTS");
118
+ fault();
170
119
  return value;
171
120
  }
172
- function assertExactOptionCount(options, minimum, maximum = minimum) {
121
+ function count(options, minimum, maximum = minimum) {
173
122
  const count = Object.keys(options).length;
174
- if (count < minimum || count > maximum) {
175
- throw new CliFault("INVALID_ARGUMENTS");
176
- }
123
+ if (count < minimum || count > maximum)
124
+ fault();
177
125
  }
178
126
  function requireAlias(options, name) {
179
127
  const alias = requireString(options, name);
180
128
  if (!isGatewayAlias(alias))
181
- throw new CliFault("INVALID_ARGUMENTS");
129
+ fault();
182
130
  return alias;
183
131
  }
184
132
  function requireCodexAlias(options, name) {
185
133
  const alias = requireAlias(options, name);
186
134
  if (!alias.startsWith("codex-"))
187
- throw new CliFault("INVALID_ARGUMENTS");
135
+ fault();
188
136
  return alias;
189
137
  }
190
- function gatewayAliasHost(alias) {
191
- return alias.slice(alias.lastIndexOf("@") + 1);
192
- }
138
+ const gatewayAliasHost = (alias) => alias.slice(alias.lastIndexOf("@") + 1);
193
139
  function requirePairAliases(options) {
194
140
  const from = requireAlias(options, "from");
195
141
  const to = requireAlias(options, "to");
196
- if (from === to || gatewayAliasHost(from) !== gatewayAliasHost(to)) {
197
- throw new CliFault("INVALID_ARGUMENTS");
198
- }
142
+ if (from === to || gatewayAliasHost(from) !== gatewayAliasHost(to))
143
+ fault();
199
144
  return [from, to];
200
145
  }
201
146
  function requireClaudeSelector(options, name) {
202
147
  const selector = requireString(options, name);
203
- if (!isClaudeSessionSelector(selector)) {
204
- throw new CliFault("INVALID_ARGUMENTS");
205
- }
148
+ if (!isClaudeSessionSelector(selector))
149
+ fault();
206
150
  return selector;
207
151
  }
208
152
  function trackIdleMinutes(options) {
209
153
  const tracking = options.track === true;
210
154
  const raw = options["idle-minutes"];
211
- if (raw !== undefined && !tracking) {
212
- throw new CliFault("INVALID_ARGUMENTS");
213
- }
155
+ if (raw !== undefined && !tracking)
156
+ fault();
214
157
  if (!tracking)
215
158
  return undefined;
216
159
  if (raw === undefined)
217
160
  return PROGRESS_WATCH_DEFAULT_IDLE_MS / 60_000;
218
- if (typeof raw !== "string" ||
219
- !/^[1-9][0-9]{0,3}$/.test(raw)) {
220
- throw new CliFault("INVALID_ARGUMENTS");
221
- }
161
+ if (typeof raw !== "string" || !/^[1-9][0-9]{0,3}$/.test(raw))
162
+ fault();
222
163
  const minutes = Number(raw);
223
- if (!Number.isSafeInteger(minutes) || minutes > 24 * 60) {
224
- throw new CliFault("INVALID_ARGUMENTS");
225
- }
164
+ if (!Number.isSafeInteger(minutes) || minutes > 24 * 60)
165
+ fault();
226
166
  return minutes;
227
167
  }
228
168
  function requireDeliveryToken(options, name) {
229
169
  const token = requireString(options, name);
230
- if (!isGatewayDeliveryToken(token)) {
231
- throw new CliFault("INVALID_ARGUMENTS");
232
- }
170
+ if (!isGatewayDeliveryToken(token))
171
+ fault();
233
172
  return token;
234
173
  }
235
174
  function requireCodexThreadId(env) {
236
175
  const threadId = env.CODEX_THREAD_ID;
237
- if (typeof threadId !== "string" || !THREAD_ID_PATTERN.test(threadId)) {
238
- throw new CliFault("CODEX_IDENTITY_REQUIRED");
239
- }
176
+ if (typeof threadId !== "string" || !THREAD_ID_PATTERN.test(threadId))
177
+ fault("CODEX_IDENTITY_REQUIRED");
240
178
  return threadId.toLowerCase();
241
179
  }
242
- function hasInheritedIdentity(value) {
243
- return typeof value === "string" && value.length > 0;
244
- }
180
+ const hasIdentity = (value) => typeof value === "string" && value.length > 0;
245
181
  function callerIdentityConflictFault(env) {
246
- const hasBothIdentities = hasInheritedIdentity(env.CODEX_THREAD_ID) &&
247
- hasInheritedIdentity(env.CLAUDE_CODE_MESSAGING_SOCKET);
248
- return new CliFault("CALLER_IDENTITY_CONFLICT", false, hasBothIdentities ? "callerIdentityConflict" : undefined);
182
+ const both = hasIdentity(env.CODEX_THREAD_ID) && hasIdentity(env.CLAUDE_CODE_MESSAGING_SOCKET);
183
+ return new CliFault("CALLER_IDENTITY_CONFLICT", false, both ? "callerIdentityConflict" : undefined);
249
184
  }
250
185
  function requireExclusiveCodexThreadId(env) {
251
- if (hasInheritedIdentity(env.CLAUDE_CODE_MESSAGING_SOCKET)) {
186
+ if (hasIdentity(env.CLAUDE_CODE_MESSAGING_SOCKET))
252
187
  throw callerIdentityConflictFault(env);
253
- }
254
188
  return requireCodexThreadId(env);
255
189
  }
256
190
  function optionalCodexThreadId(env) {
257
191
  const threadId = env.CODEX_THREAD_ID;
258
192
  if (threadId === undefined || threadId.length === 0)
259
193
  return undefined;
260
- if (!THREAD_ID_PATTERN.test(threadId)) {
261
- throw new CliFault("CODEX_IDENTITY_REQUIRED");
262
- }
194
+ if (!THREAD_ID_PATTERN.test(threadId))
195
+ fault("CODEX_IDENTITY_REQUIRED");
263
196
  return threadId.toLowerCase();
264
197
  }
265
198
  function optionalClaudeReplyAddress(env) {
266
199
  const socketPath = env.CLAUDE_CODE_MESSAGING_SOCKET;
267
200
  if (socketPath === undefined || socketPath.length === 0)
268
201
  return undefined;
269
- // Claude Code 2.1.225 exports the raw socket path. The peer protocol's
270
- // transient callback capability is the same path with an explicit UDS
271
- // scheme; form it only in memory and leave generation validation to the
272
- // broker's pinned Claude adapter.
273
- if (socketPath.includes("\0") ||
274
- !path.isAbsolute(socketPath) ||
275
- path.resolve(socketPath) !== socketPath) {
276
- throw new CliFault("CLAUDE_IDENTITY_INVALID");
277
- }
202
+ if (socketPath.includes("\0") || !path.isAbsolute(socketPath) || path.resolve(socketPath) !== socketPath)
203
+ fault("CLAUDE_IDENTITY_INVALID");
278
204
  const replyAddress = `uds:${socketPath}`;
279
- if (!isGatewayReplyAddress(replyAddress)) {
280
- throw new CliFault("CLAUDE_IDENTITY_INVALID");
281
- }
205
+ if (!isGatewayReplyAddress(replyAddress))
206
+ fault("CLAUDE_IDENTITY_INVALID");
282
207
  return replyAddress;
283
208
  }
284
209
  function requireClaudeReplyAddress(env) {
285
210
  const replyAddress = optionalClaudeReplyAddress(env);
286
- if (replyAddress === undefined) {
287
- throw new CliFault("CLAUDE_IDENTITY_REQUIRED");
288
- }
211
+ if (replyAddress === undefined)
212
+ fault("CLAUDE_IDENTITY_REQUIRED");
289
213
  return replyAddress;
290
214
  }
291
215
  function requireExclusiveClaudeReplyAddress(env) {
292
- if (hasInheritedIdentity(env.CODEX_THREAD_ID)) {
216
+ if (hasIdentity(env.CODEX_THREAD_ID))
293
217
  throw callerIdentityConflictFault(env);
294
- }
295
218
  return requireClaudeReplyAddress(env);
296
219
  }
297
220
  async function readMessageBody(stdin) {
298
221
  const chunks = [];
299
222
  let length = 0;
300
223
  for await (const chunk of stdin) {
301
- if (typeof chunk !== "string" && !Buffer.isBuffer(chunk)) {
302
- throw new CliFault("INVALID_MESSAGE_INPUT");
303
- }
304
- const buffer = Buffer.isBuffer(chunk)
305
- ? Buffer.from(chunk)
306
- : Buffer.from(chunk, "utf8");
224
+ if (typeof chunk !== "string" && !Buffer.isBuffer(chunk))
225
+ fault("INVALID_MESSAGE_INPUT");
226
+ const buffer = Buffer.from(chunk);
307
227
  length += buffer.length;
308
- if (length > GATEWAY_CONTROL_MAX_MESSAGE_BYTES) {
228
+ if (length > GATEWAY_CONTROL_MAX_MESSAGE_BYTES)
309
229
  throw new CliFault("MESSAGE_TOO_LARGE", false, "hint.messageTooLarge");
310
- }
311
230
  chunks.push(buffer);
312
231
  }
313
232
  let text;
@@ -315,685 +234,422 @@ async function readMessageBody(stdin) {
315
234
  text = new TextDecoder("utf-8", { fatal: true }).decode(Buffer.concat(chunks, length));
316
235
  }
317
236
  catch {
318
- throw new CliFault("INVALID_MESSAGE_INPUT");
319
- }
320
- if (text.trim().length === 0 || text.includes("\0")) {
321
- throw new CliFault("MESSAGE_REQUIRED");
237
+ fault("INVALID_MESSAGE_INPUT");
322
238
  }
239
+ if (text.trim().length === 0 || text.includes("\0"))
240
+ fault("MESSAGE_REQUIRED");
323
241
  return text;
324
242
  }
325
- function emptyParams(args) {
326
- if (args.length !== 0)
327
- throw new CliFault("INVALID_ARGUMENTS");
328
- return {};
329
- }
243
+ const emptyParams = (args) => args.length === 0 ? {} : fault();
330
244
  function parseServeInboundMode(args) {
331
245
  const options = parseOptions(args, ["inbound"]);
332
246
  if (Object.keys(options).length === 0)
333
247
  return "paired";
334
- assertExactOptionCount(options, 1);
248
+ count(options, 1);
335
249
  if (options.inbound !== "open")
336
- throw new CliFault("INVALID_ARGUMENTS");
250
+ fault();
337
251
  return "open";
338
252
  }
339
253
  function parseLiveDashboardPort(value) {
340
254
  if (value === undefined)
341
255
  return DEFAULT_LIVE_DASHBOARD_PORT;
342
- if (typeof value !== "string" || !/^[0-9]+$/.test(value)) {
343
- throw new CliFault("INVALID_ARGUMENTS");
344
- }
256
+ if (typeof value !== "string" || !/^[0-9]+$/.test(value))
257
+ fault();
345
258
  const port = Number(value);
346
- if (!Number.isSafeInteger(port) || port < 1_024 || port > 65_535) {
347
- throw new CliFault("INVALID_ARGUMENTS");
348
- }
259
+ if (!Number.isSafeInteger(port) || port < 1_024 || port > 65_535)
260
+ fault();
349
261
  return port;
350
262
  }
351
263
  function parseLiveDashboardArgs(args) {
352
- if (args.length === 0) {
264
+ if (args.length === 0)
353
265
  throw new CliFault("INVALID_ARGUMENTS", false, "hint.dashboardLiveRequired");
354
- }
355
266
  const options = parseOptions(args, ["port"], ["live"]);
356
- assertExactOptionCount(options, 1, 2);
267
+ count(options, 1, 2);
357
268
  if (options.live !== true)
358
- throw new CliFault("INVALID_ARGUMENTS");
269
+ fault();
359
270
  return parseLiveDashboardPort(options.port);
360
271
  }
272
+ const envelope = (method, params) => ({ protocolVersion: GATEWAY_CONTROL_PROTOCOL_VERSION, method, params });
361
273
  async function buildRequest(command, args, env, stdin) {
274
+ const simple = {
275
+ health: "health", status: "list_snapshot", doctor: "list_snapshot",
276
+ "refresh-dashboard": "refresh_dashboard",
277
+ };
278
+ const simpleMethod = simple[command];
279
+ if (simpleMethod !== undefined)
280
+ return envelope(simpleMethod, emptyParams(args));
362
281
  switch (command) {
363
282
  case "serve":
364
283
  case "dashboard":
365
- throw new CliFault("INVALID_ARGUMENTS");
284
+ case "convert-state-v2-to-v3":
285
+ return fault();
366
286
  case "health":
367
- return {
368
- protocolVersion: GATEWAY_CONTROL_PROTOCOL_VERSION,
369
- method: "health",
370
- params: emptyParams(args),
371
- };
372
287
  case "status":
373
288
  case "doctor":
374
- return {
375
- protocolVersion: GATEWAY_CONTROL_PROTOCOL_VERSION,
376
- method: "list_snapshot",
377
- params: emptyParams(args),
378
- };
289
+ case "refresh-dashboard": return fault();
379
290
  case "delivery-status":
380
291
  case "wait-delivery": {
381
292
  const options = parseOptions(args, ["token"]);
382
- assertExactOptionCount(options, 1);
383
- return {
384
- protocolVersion: GATEWAY_CONTROL_PROTOCOL_VERSION,
385
- method: "delivery_status",
386
- params: { token: requireDeliveryToken(options, "token") },
387
- };
293
+ count(options, 1);
294
+ return envelope("delivery_status", { token: requireDeliveryToken(options, "token") });
388
295
  }
389
296
  case "untrack": {
390
297
  const options = parseOptions(args, ["conversation"]);
391
- assertExactOptionCount(options, 1);
298
+ count(options, 1);
392
299
  const conversationId = requireString(options, "conversation");
393
- if (!isGatewayConversationId(conversationId)) {
394
- throw new CliFault("INVALID_ARGUMENTS");
395
- }
396
- return {
397
- protocolVersion: GATEWAY_CONTROL_PROTOCOL_VERSION,
398
- method: "untrack",
399
- params: { conversationId },
400
- };
300
+ if (!isGatewayConversationId(conversationId))
301
+ fault();
302
+ return envelope("untrack", { conversationId });
401
303
  }
402
- case "refresh-dashboard":
403
- return {
404
- protocolVersion: GATEWAY_CONTROL_PROTOCOL_VERSION,
405
- method: "refresh_dashboard",
406
- params: emptyParams(args),
407
- };
408
304
  case "register-codex": {
409
305
  const options = parseOptions(args, ["alias", "host", "succeeds"]);
410
306
  const alias = requireCodexAlias(options, "alias");
411
- const succeedsAlias = options.succeeds === undefined
412
- ? undefined
413
- : requireCodexAlias(options, "succeeds");
414
- if (succeedsAlias === undefined) {
415
- assertExactOptionCount(options, 1, 2);
416
- }
417
- else {
418
- assertExactOptionCount(options, 2);
419
- if (options.host !== undefined) {
420
- throw new CliFault("INVALID_ARGUMENTS");
421
- }
422
- }
423
- const host = succeedsAlias === undefined
424
- ? (options.host ?? DEFAULT_HOST_ID)
425
- : gatewayAliasHost(alias);
307
+ const succeedsAlias = options.succeeds === undefined ? undefined : requireCodexAlias(options, "succeeds");
308
+ count(options, succeedsAlias === undefined ? 1 : 2, 2);
309
+ if (succeedsAlias !== undefined && options.host !== undefined)
310
+ fault();
311
+ const host = succeedsAlias === undefined ? (options.host ?? DEFAULT_HOST_ID) : gatewayAliasHost(alias);
426
312
  if (typeof host !== "string" ||
427
313
  !isGatewayHostId(host) ||
428
314
  !alias.endsWith(`@${host}`) ||
429
- (succeedsAlias !== undefined &&
430
- (succeedsAlias === alias ||
431
- gatewayAliasHost(succeedsAlias) !== host))) {
432
- throw new CliFault("INVALID_ARGUMENTS");
433
- }
434
- return {
435
- protocolVersion: GATEWAY_CONTROL_PROTOCOL_VERSION,
436
- method: "register_codex",
437
- params: {
438
- alias,
439
- threadId: requireExclusiveCodexThreadId(env),
440
- hostId: host,
441
- busyPolicy: "queue",
442
- ...(succeedsAlias === undefined ? {} : { succeedsAlias }),
443
- },
444
- };
315
+ (succeedsAlias !== undefined && (succeedsAlias === alias || gatewayAliasHost(succeedsAlias) !== host)))
316
+ fault();
317
+ return envelope("register_codex", {
318
+ alias, threadId: requireExclusiveCodexThreadId(env), hostId: host, busyPolicy: "queue",
319
+ ...(succeedsAlias === undefined ? {} : { succeedsAlias }),
320
+ });
445
321
  }
446
322
  case "unregister-codex": {
447
323
  const options = parseOptions(args, ["alias"]);
448
- assertExactOptionCount(options, 1);
449
- return {
450
- protocolVersion: GATEWAY_CONTROL_PROTOCOL_VERSION,
451
- method: "unregister_codex",
452
- params: {
453
- alias: requireCodexAlias(options, "alias"),
454
- threadId: requireExclusiveCodexThreadId(env),
455
- },
456
- };
324
+ count(options, 1);
325
+ return envelope("unregister_codex", { alias: requireCodexAlias(options, "alias"), threadId: requireExclusiveCodexThreadId(env) });
457
326
  }
458
327
  case "select-claude":
459
328
  case "unselect-claude": {
460
329
  const options = parseOptions(args, ["alias", "session"]);
461
- assertExactOptionCount(options, 1);
462
- const selector = options.alias === undefined
463
- ? requireClaudeSelector(options, "session")
464
- : requireClaudeSelector(options, "alias");
330
+ count(options, 1);
331
+ const selector = requireClaudeSelector(options, options.alias === undefined ? "session" : "alias");
465
332
  const codexThreadId = optionalCodexThreadId(env);
466
- return {
467
- protocolVersion: GATEWAY_CONTROL_PROTOCOL_VERSION,
468
- method: command === "select-claude" ? "select_claude" : "unselect_claude",
469
- params: {
470
- alias: selector,
471
- ...(codexThreadId === undefined ? {} : { codexThreadId }),
472
- },
473
- };
333
+ return envelope(command === "select-claude" ? "select_claude" : "unselect_claude", {
334
+ alias: selector, ...(codexThreadId === undefined ? {} : { codexThreadId }),
335
+ });
474
336
  }
475
337
  case "pair":
476
338
  case "unpair": {
477
339
  const options = parseOptions(args, ["claude", "codex", "from", "to"]);
478
- assertExactOptionCount(options, 2);
340
+ count(options, 2);
479
341
  const legacyArm = options.claude !== undefined || options.codex !== undefined;
480
342
  if (legacyArm) {
481
- if (options.from !== undefined || options.to !== undefined) {
482
- throw new CliFault("INVALID_ARGUMENTS");
483
- }
484
- return {
485
- protocolVersion: GATEWAY_CONTROL_PROTOCOL_VERSION,
486
- method: command,
487
- params: {
488
- claudeAlias: requireClaudeSelector(options, "claude"),
489
- codexAlias: requireCodexAlias(options, "codex"),
490
- codexThreadId: requireExclusiveCodexThreadId(env),
491
- },
492
- };
343
+ if (options.from !== undefined || options.to !== undefined)
344
+ fault();
345
+ return envelope(command, {
346
+ claudeAlias: requireClaudeSelector(options, "claude"), codexAlias: requireCodexAlias(options, "codex"),
347
+ codexThreadId: requireExclusiveCodexThreadId(env),
348
+ });
493
349
  }
494
- return {
495
- protocolVersion: GATEWAY_CONTROL_PROTOCOL_VERSION,
496
- method: command,
497
- params: {
498
- aliases: requirePairAliases(options),
499
- },
500
- };
501
- }
502
- case "send-to-claude": {
503
- const options = parseOptions(args, ["from", "to", "idle-minutes"], ["expects-reply", "track"]);
504
- assertExactOptionCount(options, 2, 5);
505
- const threadId = requireExclusiveCodexThreadId(env);
506
- const idleMinutes = trackIdleMinutes(options);
507
- return {
508
- protocolVersion: GATEWAY_CONTROL_PROTOCOL_VERSION,
509
- method: "send_to_claude",
510
- params: {
511
- fromAlias: requireAlias(options, "from"),
512
- threadId,
513
- toAlias: requireClaudeSelector(options, "to"),
514
- text: await readMessageBody(stdin),
515
- expectsReply: options["expects-reply"] === true,
516
- ...(idleMinutes === undefined
517
- ? {}
518
- : { trackIdleMinutes: idleMinutes }),
519
- },
520
- };
350
+ return envelope(command, { aliases: requirePairAliases(options) });
521
351
  }
352
+ case "send-to-claude":
522
353
  case "send-to-codex": {
523
354
  const options = parseOptions(args, ["from", "to", "idle-minutes"], ["expects-reply", "track"]);
524
- assertExactOptionCount(options, 2, 5);
525
- const replyAddress = requireExclusiveClaudeReplyAddress(env);
355
+ count(options, 2, 5);
356
+ const authority = command === "send-to-claude"
357
+ ? requireExclusiveCodexThreadId(env) : requireExclusiveClaudeReplyAddress(env);
526
358
  const idleMinutes = trackIdleMinutes(options);
527
- return {
528
- protocolVersion: GATEWAY_CONTROL_PROTOCOL_VERSION,
529
- method: "send_to_codex",
530
- params: {
531
- fromAlias: requireAlias(options, "from"),
532
- toAlias: requireAlias(options, "to"),
533
- text: await readMessageBody(stdin),
534
- replyAddress,
535
- expectsReply: options["expects-reply"] === true,
536
- ...(idleMinutes === undefined
537
- ? {}
538
- : { trackIdleMinutes: idleMinutes }),
539
- },
359
+ const common = {
360
+ fromAlias: requireAlias(options, "from"),
361
+ toAlias: command === "send-to-claude" ? requireClaudeSelector(options, "to") : requireAlias(options, "to"),
362
+ text: await readMessageBody(stdin),
363
+ expectsReply: options["expects-reply"] === true,
364
+ ...(idleMinutes === undefined ? {} : { trackIdleMinutes: idleMinutes }),
540
365
  };
366
+ return command === "send-to-claude"
367
+ ? envelope("send_to_claude", { ...common, threadId: authority })
368
+ : envelope("send_to_codex", { ...common, replyAddress: authority });
541
369
  }
542
370
  case "reply": {
543
371
  const options = parseOptions(args, ["conversation", "alias", "idle-minutes"], ["track"]);
544
- assertExactOptionCount(options, 2, 4);
372
+ count(options, 2, 4);
545
373
  const conversationId = requireString(options, "conversation");
546
- if (!isGatewayConversationId(conversationId)) {
547
- throw new CliFault("INVALID_ARGUMENTS");
548
- }
374
+ if (!isGatewayConversationId(conversationId))
375
+ fault();
549
376
  const alias = requireAlias(options, "alias");
550
377
  const idleMinutes = trackIdleMinutes(options);
551
378
  const threadId = env.CODEX_THREAD_ID;
552
- if (hasInheritedIdentity(threadId) &&
553
- hasInheritedIdentity(env.CLAUDE_CODE_MESSAGING_SOCKET)) {
379
+ if (hasIdentity(threadId) && hasIdentity(env.CLAUDE_CODE_MESSAGING_SOCKET))
554
380
  throw callerIdentityConflictFault(env);
555
- }
556
381
  const replyAddress = optionalClaudeReplyAddress(env);
557
- const hasCodexIdentity = hasInheritedIdentity(threadId);
558
- const hasClaudeIdentity = replyAddress !== undefined;
559
- if (!hasCodexIdentity && !hasClaudeIdentity) {
560
- throw new CliFault("CALLER_IDENTITY_REQUIRED");
561
- }
562
- return {
563
- protocolVersion: GATEWAY_CONTROL_PROTOCOL_VERSION,
564
- method: "reply",
565
- params: {
566
- conversationId,
567
- text: await readMessageBody(stdin),
568
- ...(idleMinutes === undefined
569
- ? {}
570
- : { trackIdleMinutes: idleMinutes }),
571
- caller: hasCodexIdentity
572
- ? {
573
- kind: "codex",
574
- alias,
575
- threadId: requireCodexThreadId(env),
576
- }
577
- : {
578
- kind: "claude",
579
- alias,
580
- replyAddress: requireClaudeReplyAddress(env),
581
- },
582
- },
583
- };
382
+ const codex = hasIdentity(threadId);
383
+ if (!codex && replyAddress === undefined)
384
+ fault("CALLER_IDENTITY_REQUIRED");
385
+ return envelope("reply", {
386
+ conversationId, text: await readMessageBody(stdin),
387
+ ...(idleMinutes === undefined ? {} : { trackIdleMinutes: idleMinutes }),
388
+ caller: codex
389
+ ? { kind: "codex", alias, threadId: requireCodexThreadId(env) }
390
+ : { kind: "claude", alias, replyAddress: requireClaudeReplyAddress(env) },
391
+ });
584
392
  }
585
393
  }
586
394
  }
587
395
  export async function validatePrivateGatewayControlSocket(stateDir, socketPath) {
588
- if (process.platform === "win32") {
396
+ if (process.platform === "win32")
589
397
  throw new CliFault("CONTROL_SOCKET_UNAVAILABLE", true);
590
- }
591
- let state;
592
- let socket;
398
+ let state, socket;
593
399
  try {
594
400
  [state, socket] = await Promise.all([lstat(stateDir), lstat(socketPath)]);
595
401
  }
596
402
  catch {
597
403
  throw new CliFault("CONTROL_SOCKET_UNAVAILABLE", true);
598
404
  }
599
- const getuid = process.getuid;
600
- const expectedUid = getuid?.call(process);
601
- if (state.isSymbolicLink() ||
602
- !state.isDirectory() ||
603
- (state.mode & 0o700) !== 0o700 ||
604
- (state.mode & 0o077) !== 0 ||
605
- (expectedUid !== undefined && state.uid !== expectedUid)) {
405
+ const uid = process.getuid?.();
406
+ if (state.isSymbolicLink() || !state.isDirectory() || (state.mode & 0o777) !== 0o700 || (uid !== undefined && state.uid !== uid)) {
606
407
  throw new CliFault("CONTROL_STATE_UNSAFE", false, undefined, "unsafe");
607
408
  }
608
- if (socket.isSymbolicLink() ||
609
- !socket.isSocket() ||
610
- (socket.mode & 0o777) !== 0o600 ||
611
- (expectedUid !== undefined && socket.uid !== expectedUid)) {
409
+ if (socket.isSymbolicLink() || !socket.isSocket() || (socket.mode & 0o777) !== 0o600 || (uid !== undefined && socket.uid !== uid)) {
612
410
  throw new CliFault("CONTROL_SOCKET_UNSAFE", false, undefined, "unsafe");
613
411
  }
614
- let stateReal;
615
- let socketParentReal;
412
+ let stateReal, parentReal;
616
413
  try {
617
- [stateReal, socketParentReal] = await Promise.all([
618
- realpath(stateDir),
619
- realpath(path.dirname(socketPath)),
620
- ]);
414
+ [stateReal, parentReal] = await Promise.all([realpath(stateDir), realpath(path.dirname(socketPath))]);
621
415
  }
622
416
  catch {
623
417
  throw new CliFault("CONTROL_SOCKET_UNSAFE", false, undefined, "unsafe");
624
418
  }
625
- if (path.dirname(socketPath) !== stateDir ||
626
- stateReal !== socketParentReal) {
419
+ if (path.dirname(socketPath) !== stateDir || stateReal !== parentReal) {
627
420
  throw new CliFault("CONTROL_SOCKET_UNSAFE", false, undefined, "unsafe");
628
421
  }
629
422
  }
630
- function safeCommandLabel(command) {
631
- return command ?? "unknown";
632
- }
633
423
  function serializedOutput(value) {
634
424
  const line = `${JSON.stringify(value)}\n`;
635
- if (Buffer.byteLength(line, "utf8") > CLI_MAX_OUTPUT_BYTES) {
636
- throw new CliFault("OUTPUT_TOO_LARGE");
637
- }
425
+ if (Buffer.byteLength(line, "utf8") > CLI_MAX_OUTPUT_BYTES)
426
+ fault("OUTPUT_TOO_LARGE");
638
427
  return line;
639
428
  }
640
429
  function writeFailure(stdout, stderr, locale, command, code, options) {
641
- const ambiguous = options.ambiguous ?? false;
642
- const retryable = options.retryable ?? false;
643
- stdout.write(serializedOutput({
644
- ok: false,
645
- command: safeCommandLabel(command),
646
- error: { code, ambiguous, retryable },
647
- }));
430
+ stdout.write(serializedOutput({ ok: false, command: command ?? "unknown", error: {
431
+ code, ambiguous: options.ambiguous ?? false, retryable: options.retryable ?? false,
432
+ } }));
648
433
  stderr.write(fixedStderr(locale, options.kind));
649
434
  }
650
435
  function isRejectedResult(result) {
651
- return (result !== null &&
652
- typeof result === "object" &&
653
- Object.hasOwn(result, "accepted") &&
654
- result.accepted === false);
436
+ return result !== null && typeof result === "object" && result.accepted === false;
655
437
  }
656
438
  function isProgressWatchOwnerConflict(result) {
657
- return (result !== null &&
658
- typeof result === "object" &&
659
- Object.hasOwn(result, "accepted") &&
660
- Object.hasOwn(result, "code") &&
661
- result.accepted === false &&
662
- result.code === "watch_owner_conflict");
439
+ return isRejectedResult(result) && result.code === "watch_owner_conflict";
663
440
  }
664
441
  function responseExitCode(response) {
665
- if (!response.ok)
666
- return gatewayCliExitCodes.failure;
667
- if (isRejectedResult(response.result))
668
- return gatewayCliExitCodes.rejected;
669
- return gatewayCliExitCodes.ok;
442
+ return !response.ok ? gatewayCliExitCodes.failure
443
+ : isRejectedResult(response.result) ? gatewayCliExitCodes.rejected : gatewayCliExitCodes.ok;
670
444
  }
671
445
  function codexDoctorConditions(result) {
672
- if (result === null ||
673
- typeof result !== "object" ||
674
- !Object.hasOwn(result, "connectors") ||
675
- !Array.isArray(result.connectors)) {
676
- return ["unknown"];
677
- }
678
- const connector = result.connectors.find((row) => row !== null &&
679
- typeof row === "object" &&
680
- row.provider === "codex");
446
+ const connectors = result !== null && typeof result === "object"
447
+ ? result.connectors : undefined;
448
+ const connector = Array.isArray(connectors) ? connectors.find((row) => row !== null && typeof row === "object" && row.provider === "codex") : undefined;
681
449
  const doctor = connector !== null && typeof connector === "object"
682
- ? connector.codexDoctor
683
- : undefined;
684
- if (doctor === null ||
685
- typeof doctor !== "object" ||
686
- !Array.isArray(doctor.conditions)) {
687
- return ["unknown"];
688
- }
689
- return doctor.conditions.filter((condition) => typeof condition === "string");
450
+ ? connector.codexDoctor : undefined;
451
+ const conditions = doctor !== null && typeof doctor === "object"
452
+ ? doctor.conditions : undefined;
453
+ return Array.isArray(conditions)
454
+ ? conditions.filter((value) => typeof value === "string") : ["unknown"];
690
455
  }
691
456
  function waitDeliveryExitCode(response) {
692
- if (!response.ok ||
693
- !response.result.found ||
694
- !response.result.terminal) {
695
- return gatewayCliExitCodes.failure;
696
- }
697
- return response.result.state === "delivered"
698
- ? gatewayCliExitCodes.ok
699
- : gatewayCliExitCodes.failure;
700
- }
701
- async function defaultDelay(milliseconds) {
702
- await new Promise((resolve) => setTimeout(resolve, milliseconds));
457
+ return response.ok && response.result.found && response.result.terminal && response.result.state === "delivered"
458
+ ? gatewayCliExitCodes.ok : gatewayCliExitCodes.failure;
703
459
  }
460
+ const defaultDelay = async (milliseconds) => await new Promise((resolve) => setTimeout(resolve, milliseconds));
704
461
  async function waitForDelivery(socketPath, request, sendRequest, now, delay) {
705
- let clientDeadlineMs;
462
+ let deadline;
706
463
  while (true) {
707
- const beforeRequest = now();
708
- let requestTimeoutMs = GATEWAY_CONTROL_DEFAULT_TIMEOUT_MS;
709
- if (clientDeadlineMs !== undefined) {
710
- const remainingMs = clientDeadlineMs - beforeRequest;
711
- if (remainingMs <= 0)
712
- return { kind: "timeout" };
713
- if (remainingMs < DELIVERY_POLL_MIN_REQUEST_TIMEOUT_MS) {
714
- await delay(remainingMs);
715
- return { kind: "timeout" };
716
- }
717
- requestTimeoutMs = Math.min(GATEWAY_CONTROL_DEFAULT_TIMEOUT_MS, Math.floor(remainingMs));
464
+ const remaining = deadline === undefined ? undefined : deadline - now();
465
+ if (remaining !== undefined && remaining <= 0)
466
+ return { kind: "timeout" };
467
+ if (remaining !== undefined && remaining < DELIVERY_POLL_MIN_REQUEST_TIMEOUT_MS) {
468
+ await delay(remaining);
469
+ return { kind: "timeout" };
718
470
  }
719
471
  let response;
720
472
  try {
721
473
  response = await sendRequest({
722
- socketPath,
723
- request,
724
- timeoutMs: requestTimeoutMs,
474
+ socketPath, request,
475
+ timeoutMs: remaining === undefined ? GATEWAY_CONTROL_DEFAULT_TIMEOUT_MS
476
+ : Math.min(GATEWAY_CONTROL_DEFAULT_TIMEOUT_MS, Math.floor(remaining)),
725
477
  });
726
478
  }
727
479
  catch (error) {
728
- if (clientDeadlineMs !== undefined &&
729
- now() >= clientDeadlineMs &&
730
- error instanceof GatewayControlTransportError &&
731
- error.recoverable) {
480
+ if (deadline !== undefined && now() >= deadline && error instanceof GatewayControlTransportError && error.recoverable)
732
481
  return { kind: "timeout" };
733
- }
734
482
  throw error;
735
483
  }
736
484
  if (!response.ok) {
737
- if (response.error.code === "REQUEST_TIMEOUT" &&
738
- clientDeadlineMs !== undefined &&
739
- now() >= clientDeadlineMs) {
485
+ if (response.error.code === "REQUEST_TIMEOUT" && deadline !== undefined && now() >= deadline)
740
486
  return { kind: "timeout" };
741
- }
742
487
  return { kind: "response", response };
743
488
  }
744
489
  if (!response.result.found)
745
490
  return { kind: "unknown" };
746
- // A retained terminal decision is authoritative even when the command is
747
- // invoked after the original delivery deadline and client grace window.
748
- if (response.result.terminal) {
491
+ if (response.result.terminal)
749
492
  return { kind: "response", response };
750
- }
751
- const parsedDeadlineMs = Date.parse(response.result.deadlineAt);
752
- if (!Number.isFinite(parsedDeadlineMs)) {
493
+ const observed = Date.parse(response.result.deadlineAt) + GATEWAY_CONTROL_DEFAULT_TIMEOUT_MS;
494
+ if (!Number.isFinite(observed))
753
495
  throw new Error("invalid validated delivery deadline");
754
- }
755
- const observedClientDeadlineMs = parsedDeadlineMs + GATEWAY_CONTROL_DEFAULT_TIMEOUT_MS;
756
- clientDeadlineMs =
757
- clientDeadlineMs === undefined
758
- ? observedClientDeadlineMs
759
- : Math.min(clientDeadlineMs, observedClientDeadlineMs);
760
- const afterRequest = now();
761
- const remainingMs = clientDeadlineMs - afterRequest;
762
- if (remainingMs <= 0)
496
+ deadline = deadline === undefined ? observed : Math.min(deadline, observed);
497
+ const after = deadline - now();
498
+ if (after <= 0)
763
499
  return { kind: "timeout" };
764
- await delay(Math.min(DELIVERY_POLL_INTERVAL_MS, remainingMs));
500
+ await delay(Math.min(DELIVERY_POLL_INTERVAL_MS, after));
765
501
  }
766
502
  }
767
503
  /** Run one command; foreground runners own and release their signal handlers. */
768
504
  export async function runGatewayCli(argv = process.argv.slice(2), dependencies = {}) {
769
505
  const env = dependencies.env ?? process.env;
770
- const stdin = dependencies.stdin ?? process.stdin;
771
- const stdout = dependencies.stdout ?? process.stdout;
772
- const stderr = dependencies.stderr ?? process.stderr;
773
- const loadConfig = dependencies.loadConfig ?? loadGatewayConfig;
506
+ const stdin = dependencies.stdin ?? process.stdin, stdout = dependencies.stdout ?? process.stdout;
507
+ const stderr = dependencies.stderr ?? process.stderr, loadConfig = dependencies.loadConfig ?? loadGatewayConfig;
774
508
  const sendRequest = dependencies.sendRequest ?? sendGatewayControlRequest;
775
- const validateControlSocket = dependencies.validateControlSocket ?? validatePrivateGatewayControlSocket;
776
- const runServer = dependencies.runServer ?? runGatewayServer;
777
- const runLiveDashboard = dependencies.runLiveDashboard ?? runLiveDashboardCommand;
778
- const now = dependencies.now ?? Date.now;
779
- const delay = dependencies.delay ?? defaultDelay;
780
- if (argv.length === 1 &&
781
- (argv[0] === "--version" || argv[0] === "-v")) {
509
+ const validateSocket = dependencies.validateControlSocket ?? validatePrivateGatewayControlSocket;
510
+ if (argv.length === 1 && (argv[0] === "--version" || argv[0] === "-v")) {
782
511
  stdout.write(`embassy ${EMBASSY_VERSION}\n`);
783
512
  return gatewayCliExitCodes.ok;
784
513
  }
785
514
  const command = isCommand(argv[0]) ? argv[0] : undefined;
786
515
  let locale = fallbackCliLocale(argv.slice(1), env);
787
- let serverReadyEmitted = false;
788
- let liveDashboardReadyEmitted = false;
516
+ let serverReady = false, dashboardReady = false;
789
517
  let liveDashboardPort;
518
+ const success = (result) => {
519
+ stdout.write(serializedOutput({ ok: true, command: command, result }));
520
+ };
790
521
  try {
791
- const common = resolveCommonCliOptions(argv.slice(1), env);
522
+ const common = commonOptions(argv.slice(1), env);
792
523
  locale = common.locale;
793
- if (argv.length === 0 ||
794
- argv[0] === "--help" ||
795
- argv[0] === "-h") {
524
+ if (argv.length === 0 || argv[0] === "--help" || argv[0] === "-h") {
796
525
  emptyParams(common.args);
797
526
  stdout.write(getCliCopy(locale)["help.usage"]);
798
527
  return gatewayCliExitCodes.ok;
799
528
  }
800
529
  if (command === undefined)
801
- throw new CliFault("UNKNOWN_COMMAND");
530
+ fault("UNKNOWN_COMMAND");
802
531
  if (command === "serve") {
803
- const inboundMode = parseServeInboundMode(common.args);
804
- await runServer({
805
- env,
806
- locale,
807
- inboundMode,
808
- ...(dependencies.serverSignal === undefined
809
- ? {}
810
- : { signal: dependencies.serverSignal }),
532
+ await (dependencies.runServer ?? runGatewayServer)({
533
+ env, locale, inboundMode: parseServeInboundMode(common.args),
534
+ ...(dependencies.serverSignal === undefined ? {} : { signal: dependencies.serverSignal }),
811
535
  onReady: async (result) => {
812
- if (serverReadyEmitted) {
813
- throw new CliFault("SERVER_READY_ALREADY_EMITTED");
814
- }
815
- stdout.write(serializedOutput({
816
- ok: true,
817
- command,
818
- result,
819
- }));
820
- serverReadyEmitted = true;
536
+ if (serverReady)
537
+ fault("SERVER_READY_ALREADY_EMITTED");
538
+ success(result);
539
+ serverReady = true;
821
540
  },
822
541
  });
823
- if (!serverReadyEmitted) {
824
- throw new CliFault("SERVER_NOT_READY");
825
- }
542
+ if (!serverReady)
543
+ fault("SERVER_NOT_READY");
544
+ return gatewayCliExitCodes.ok;
545
+ }
546
+ if (command === "convert-state-v2-to-v3") {
547
+ emptyParams(common.args);
548
+ const config = loadConfig(env);
549
+ const converted = await (dependencies.convertState ?? convertGatewayStateV2ToV3)({ stateDir: config.stateDir });
550
+ success({ converted: true, backupFile: path.basename(converted.backupFile) });
826
551
  return gatewayCliExitCodes.ok;
827
552
  }
828
553
  if (command === "dashboard") {
829
- const port = parseLiveDashboardArgs(common.args);
830
- liveDashboardPort = port;
831
- const outcome = await runLiveDashboard({
832
- env,
833
- locale,
834
- port,
835
- ...(dependencies.liveDashboardSignal === undefined
836
- ? {}
837
- : { signal: dependencies.liveDashboardSignal }),
838
- loadConfig,
839
- validateControlSocket,
840
- sendRequest,
554
+ liveDashboardPort = parseLiveDashboardArgs(common.args);
555
+ const outcome = await (dependencies.runLiveDashboard ?? runLiveDashboardCommand)({
556
+ env, locale, port: liveDashboardPort, loadConfig, sendRequest, validateControlSocket: validateSocket,
557
+ ...(dependencies.liveDashboardSignal === undefined ? {} : { signal: dependencies.liveDashboardSignal }),
841
558
  onReady: async (result) => {
842
- if (liveDashboardReadyEmitted) {
843
- throw new CliFault("LIVE_DASHBOARD_READY_ALREADY_EMITTED");
844
- }
845
- stdout.write(serializedOutput({
846
- ok: true,
847
- command,
848
- result,
849
- }));
850
- liveDashboardReadyEmitted = true;
559
+ if (dashboardReady)
560
+ fault("LIVE_DASHBOARD_READY_ALREADY_EMITTED");
561
+ success(result);
562
+ dashboardReady = true;
851
563
  },
852
564
  });
853
- if (!liveDashboardReadyEmitted) {
854
- if (outcome?.status === "cancelled") {
855
- return gatewayCliExitCodes.ok;
856
- }
565
+ if (!dashboardReady && outcome?.status === "cancelled")
566
+ return gatewayCliExitCodes.ok;
567
+ if (!dashboardReady)
857
568
  throw new Error("LIVE_DASHBOARD_NOT_READY");
858
- }
859
569
  return gatewayCliExitCodes.ok;
860
570
  }
861
571
  const request = await buildRequest(command, common.args, env, stdin);
862
572
  const config = loadConfig(env);
863
- await validateControlSocket(config.stateDir, config.controlSocketPath);
573
+ await validateSocket(config.stateDir, config.controlSocketPath);
864
574
  let response;
865
- let waitedDeliveryResponse;
575
+ let waited;
866
576
  if (command === "wait-delivery") {
867
- if (request.method !== "delivery_status") {
868
- throw new CliFault("INVALID_ARGUMENTS");
869
- }
870
- const outcome = await waitForDelivery(config.controlSocketPath, request, sendRequest, now, delay);
577
+ if (request.method !== "delivery_status")
578
+ fault();
579
+ const outcome = await waitForDelivery(config.controlSocketPath, request, sendRequest, dependencies.now ?? Date.now, dependencies.delay ?? defaultDelay);
871
580
  if (outcome.kind === "unknown") {
872
- writeFailure(stdout, stderr, locale, command, "DELIVERY_TOKEN_UNKNOWN", {
873
- kind: "tokenUnknown",
874
- });
581
+ writeFailure(stdout, stderr, locale, command, "DELIVERY_TOKEN_UNKNOWN", { kind: "tokenUnknown" });
875
582
  return gatewayCliExitCodes.rejected;
876
583
  }
877
584
  if (outcome.kind === "timeout") {
878
- writeFailure(stdout, stderr, locale, command, "DELIVERY_WAIT_TIMEOUT", {
879
- retryable: true,
880
- kind: "deliveryTimeout",
881
- });
585
+ writeFailure(stdout, stderr, locale, command, "DELIVERY_WAIT_TIMEOUT", { retryable: true, kind: "deliveryTimeout" });
882
586
  return gatewayCliExitCodes.unavailable;
883
587
  }
884
- waitedDeliveryResponse = outcome.response;
885
- response = waitedDeliveryResponse;
588
+ waited = outcome.response;
589
+ response = waited;
886
590
  }
887
591
  else {
888
- response = await sendRequest({
889
- socketPath: config.controlSocketPath,
890
- request,
891
- });
592
+ response = await sendRequest({ socketPath: config.controlSocketPath, request });
892
593
  }
893
594
  if (!response.ok) {
894
- writeFailure(stdout, stderr, locale, command, response.error.code, {
895
- kind: "failure",
896
- });
595
+ writeFailure(stdout, stderr, locale, command, response.error.code, { kind: "failure" });
897
596
  return gatewayCliExitCodes.failure;
898
597
  }
899
- const result = command === "doctor"
900
- ? { conditions: codexDoctorConditions(response.result) }
901
- : response.result;
902
- stdout.write(serializedOutput({
903
- ok: true,
904
- command,
905
- result,
906
- }));
907
- const exitCode = waitedDeliveryResponse === undefined
908
- ? responseExitCode(response)
909
- : waitDeliveryExitCode(waitedDeliveryResponse);
598
+ success(command === "doctor" ? { conditions: codexDoctorConditions(response.result) } : response.result);
599
+ const exitCode = waited === undefined ? responseExitCode(response) : waitDeliveryExitCode(waited);
910
600
  if (exitCode === gatewayCliExitCodes.rejected) {
911
601
  stderr.write(fixedStderr(locale, "decision"));
912
602
  if (isProgressWatchOwnerConflict(response.result)) {
913
603
  stderr.write(`[embassy] ${getCliCopy(locale)["hint.progressWatchOwnerConflict"]}\n`);
914
604
  }
915
- if (command === "register-codex") {
916
- const diagnosis = await sendRequest({
917
- socketPath: config.controlSocketPath,
918
- request: {
919
- protocolVersion: GATEWAY_CONTROL_PROTOCOL_VERSION,
920
- method: "list_snapshot",
921
- params: {},
922
- },
923
- }).catch(() => undefined);
924
- if (diagnosis?.ok === true) {
925
- const conditions = codexDoctorConditions(diagnosis.result);
926
- const hint = conditions.includes("split_brain")
927
- ? "hint.codexSplitBrain"
928
- : conditions.includes("orphaned")
929
- ? "hint.codexOrphaned"
930
- : undefined;
931
- if (hint !== undefined) {
932
- stderr.write(`[embassy] ${getCliCopy(locale)[hint]}\n`);
933
- }
934
- }
935
- }
936
605
  }
937
- else if (command === "wait-delivery" &&
938
- exitCode === gatewayCliExitCodes.failure) {
606
+ else if (command === "wait-delivery" && exitCode === gatewayCliExitCodes.failure)
939
607
  stderr.write(fixedStderr(locale, "failure"));
940
- }
941
608
  return exitCode;
942
609
  }
943
610
  catch (error) {
944
- if ((command === "serve" && serverReadyEmitted) ||
945
- (command === "dashboard" && liveDashboardReadyEmitted)) {
611
+ if ((command === "serve" && serverReady) || (command === "dashboard" && dashboardReady)) {
946
612
  stderr.write(fixedStderr(locale, "failure"));
947
613
  return gatewayCliExitCodes.failure;
948
614
  }
949
615
  if (error instanceof GatewayControlTransportError) {
950
616
  const ambiguous = error.ambiguous;
951
617
  writeFailure(stdout, stderr, locale, command, error.code, {
952
- ambiguous,
953
- retryable: ambiguous ? false : error.recoverable,
618
+ ambiguous, retryable: ambiguous ? false : error.recoverable,
954
619
  kind: ambiguous ? "ambiguous" : "unavailable",
955
620
  });
956
621
  if (error.code === "CONTROL_INVALID_RESPONSE") {
957
622
  stderr.write(`[embassy] ${getCliCopy(locale)["hint.controlInvalidResponse"]}\n`);
958
623
  }
959
- return ambiguous
960
- ? gatewayCliExitCodes.ambiguous
961
- : gatewayCliExitCodes.unavailable;
624
+ return ambiguous ? gatewayCliExitCodes.ambiguous : gatewayCliExitCodes.unavailable;
962
625
  }
963
626
  if (error instanceof CliFault) {
964
627
  writeFailure(stdout, stderr, locale, command, error.code, {
965
- retryable: error.retryable,
966
- kind: error.kind ?? (error.retryable ? "unavailable" : "input"),
628
+ retryable: error.retryable, kind: error.kind ?? (error.retryable ? "unavailable" : "input"),
967
629
  });
968
630
  if (error.hint !== undefined) {
969
631
  const hint = error.hint === "callerIdentityConflict"
970
- ? locale === "zh-CN"
971
- ? callerIdentityConflictHintZhCn
972
- : callerIdentityConflictHintEn
632
+ ? locale === "zh-CN" ? callerIdentityConflictHintZhCn : callerIdentityConflictHintEn
973
633
  : getCliCopy(locale)[error.hint];
974
634
  stderr.write(`[embassy] ${hint}\n`);
975
635
  }
976
- return error.retryable
977
- ? gatewayCliExitCodes.unavailable
978
- : gatewayCliExitCodes.invalidInput;
636
+ return error.retryable ? gatewayCliExitCodes.unavailable : gatewayCliExitCodes.invalidInput;
979
637
  }
980
638
  if (error instanceof BridgeError) {
639
+ if (error.code === "GATEWAY_STATE_COMMIT_OUTCOME_UNKNOWN") {
640
+ writeFailure(stdout, stderr, locale, command, error.code, { ambiguous: true, kind: "ambiguous" });
641
+ return gatewayCliExitCodes.ambiguous;
642
+ }
981
643
  writeFailure(stdout, stderr, locale, command, error.code, {
982
- retryable: error.recoverable,
983
- kind: error.recoverable ? "unavailable" : "input",
644
+ retryable: error.recoverable, kind: error.recoverable ? "unavailable" : "input",
984
645
  });
985
- if (error.code === "LIVE_DASHBOARD_PORT_IN_USE" &&
986
- liveDashboardPort !== undefined) {
646
+ if (error.code === "LIVE_DASHBOARD_PORT_IN_USE" && liveDashboardPort !== undefined) {
987
647
  const hint = getCliCopy(locale)["hint.dashboardPortInUse"].replace("{port}", String(liveDashboardPort));
988
648
  stderr.write(`[embassy] ${hint}\n`);
989
649
  }
990
- return error.recoverable
991
- ? gatewayCliExitCodes.unavailable
992
- : gatewayCliExitCodes.invalidInput;
650
+ return error.recoverable ? gatewayCliExitCodes.unavailable : gatewayCliExitCodes.invalidInput;
993
651
  }
994
- writeFailure(stdout, stderr, locale, command, "INTERNAL_ERROR", {
995
- kind: "failure",
996
- });
652
+ writeFailure(stdout, stderr, locale, command, "INTERNAL_ERROR", { kind: "failure" });
997
653
  return gatewayCliExitCodes.failure;
998
654
  }
999
655
  }