agent-embassy 1.7.0 → 1.8.0

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