@linzumi/cli 0.0.43-beta → 0.0.45-beta

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 (3) hide show
  1. package/README.md +1 -1
  2. package/dist/index.js +761 -113
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // src/index.ts
2
2
  import { randomUUID as randomUUID4 } from "node:crypto";
3
- import { existsSync as existsSync11, readFileSync as readFileSync10, realpathSync as realpathSync6 } from "node:fs";
3
+ import { existsSync as existsSync11, readFileSync as readFileSync11, realpathSync as realpathSync6 } from "node:fs";
4
4
  import { homedir as homedir9 } from "node:os";
5
5
  import { resolve as resolve9 } from "node:path";
6
6
  import { fileURLToPath as fileURLToPath3 } from "node:url";
@@ -2671,6 +2671,7 @@ function initialChannelSessionState(cursor, rootSeq, kandanThreadId, codexThread
2671
2671
  pendingApprovalRequests: new Map,
2672
2672
  approvalPromptChain: Promise.resolve(),
2673
2673
  pendingPortForwardRequests: new Map,
2674
+ portForwardPreviousProcessingStates: new Map,
2674
2675
  queuedPortForwardCandidates: new Map,
2675
2676
  approvedForwardPorts: new Set,
2676
2677
  approvedForwardTargets: new Map,
@@ -2995,6 +2996,9 @@ async function resolvePendingPortForwardRequest(args, state, payloadContext, con
2995
2996
  state.pendingPortForwardRequests.delete(control.requestId);
2996
2997
  if (control.decision === "deny") {
2997
2998
  state.dismissedForwardTargets.set(request.port, approvedTargetFromRequest(request));
2999
+ await publishForwardPortResolvedEvent(args, request, {
3000
+ decision: "deny"
3001
+ });
2998
3002
  await publishMessageStateForPortForwardResult(args, state, request, "failed");
2999
3003
  args.log("port_forward.request_denied", {
3000
3004
  request_id: control.requestId,
@@ -3014,7 +3018,10 @@ async function resolvePendingPortForwardRequest(args, state, payloadContext, con
3014
3018
  processName: processIdentity?.appName ?? null,
3015
3019
  processIconKey: processIdentity?.iconKey ?? null
3016
3020
  });
3017
- await publishForwardPortResolvedEvent(args, request, capabilities);
3021
+ await publishForwardPortResolvedEvent(args, request, {
3022
+ decision: "approve",
3023
+ capabilities
3024
+ });
3018
3025
  await publishMessageStateForPortForwardResult(args, state, request, "processed");
3019
3026
  await publishPortForwardReadyMessage(args, state, payloadContext, request);
3020
3027
  args.log("port_forward.request_approved", {
@@ -3099,12 +3106,14 @@ async function publishPortForwardPrompt(args, state, payloadContext, candidate)
3099
3106
  candidate
3100
3107
  });
3101
3108
  state.pendingPortForwardRequests.set(requestId, request);
3109
+ if (state.activeProcessingState !== undefined) {
3110
+ state.portForwardPreviousProcessingStates.set(requestId, state.activeProcessingState);
3111
+ }
3102
3112
  const processIdentity = guessCanonicalProcessFromCommand(candidate.command);
3103
3113
  const processIconPath = processIdentity?.iconKey === undefined ? undefined : `/web/process-icons/${processIdentity.iconKey}.png`;
3104
3114
  const processName = processIdentity?.appName ?? label;
3105
- await publishForwardPortRequestedEvent(args, request);
3106
- await publishMessageState(args, state.kandanThreadId, sourceSeq, {
3107
- status: "processing",
3115
+ state.activeProcessingState = {
3116
+ seq: sourceSeq,
3108
3117
  reason: "awaiting approval",
3109
3118
  approval: {
3110
3119
  requestId,
@@ -3133,7 +3142,13 @@ async function publishPortForwardPrompt(args, state, payloadContext, candidate)
3133
3142
  allowedActorSlug: args.options.channelSession.listenUser,
3134
3143
  allowedActorUserId: args.options.channelSession.listenUser.toLowerCase() === (payloadContext.runnerIdentity.actorUsername ?? "").toLowerCase() ? payloadContext.runnerIdentity.actorUserId : undefined
3135
3144
  }
3145
+ };
3146
+ await publishMessageState(args, state.kandanThreadId, sourceSeq, {
3147
+ status: "processing",
3148
+ reason: "awaiting approval",
3149
+ approval: state.activeProcessingState.approval
3136
3150
  });
3151
+ await publishForwardPortRequestedEvent(args, state, request, processIdentity);
3137
3152
  args.log("port_forward.request_pending", {
3138
3153
  request_id: requestId,
3139
3154
  port: candidate.port,
@@ -3174,6 +3189,10 @@ async function expireLostPendingPortForwardRequest(args, state, payloadContext,
3174
3189
  return;
3175
3190
  }
3176
3191
  state.pendingPortForwardRequests.delete(request.requestId);
3192
+ await publishForwardPortResolvedEvent(args, request, {
3193
+ decision: "expired",
3194
+ reason: "listener_exited"
3195
+ });
3177
3196
  await publishMessageStateForPortForwardResult(args, state, request, "failed", "port_forward_listener_exited");
3178
3197
  args.log("port_forward.pending_request_expired", {
3179
3198
  request_id: request.requestId,
@@ -3220,17 +3239,23 @@ async function publishPortForwardReadyMessage(args, state, payloadContext, reque
3220
3239
  }
3221
3240
  }, args.log);
3222
3241
  }
3223
- async function publishForwardPortRequestedEvent(args, request) {
3242
+ async function publishForwardPortRequestedEvent(args, state, request, processIdentity) {
3224
3243
  await pushOptional(args.kandan, args.topic, "forward_port_requested", {
3225
3244
  instanceId: args.instanceId,
3226
3245
  requestId: request.requestId,
3246
+ sourceSeq: request.sourceSeq,
3227
3247
  port: request.port,
3228
3248
  pid: request.pid,
3229
3249
  command: request.command,
3250
+ codexThreadId: state.codexThreadId ?? null,
3251
+ kandanThreadId: state.kandanThreadId ?? null,
3252
+ channelSlug: args.options.channelSession.channelSlug ?? null,
3253
+ ...processIdentity?.appName === undefined ? {} : { processName: processIdentity.appName },
3254
+ ...processIdentity?.iconKey === undefined ? {} : { processIconKey: processIdentity.iconKey },
3230
3255
  ...request.cwd === undefined ? {} : { cwd: request.cwd }
3231
3256
  }, args.log);
3232
3257
  }
3233
- async function publishForwardPortResolvedEvent(args, request, capabilities) {
3258
+ async function publishForwardPortResolvedEvent(args, request, result) {
3234
3259
  await pushOptional(args.kandan, args.topic, "forward_port_resolved", {
3235
3260
  instanceId: args.instanceId,
3236
3261
  requestId: request.requestId,
@@ -3238,18 +3263,52 @@ async function publishForwardPortResolvedEvent(args, request, capabilities) {
3238
3263
  pid: request.pid,
3239
3264
  command: request.command,
3240
3265
  ...request.cwd === undefined ? {} : { cwd: request.cwd },
3241
- decision: "approve",
3242
- ...capabilities === undefined ? {} : { capabilities }
3266
+ sourceSeq: request.sourceSeq,
3267
+ decision: result.decision,
3268
+ ...result.reason === undefined ? {} : { reason: result.reason },
3269
+ ...result.capabilities === undefined ? {} : { capabilities: result.capabilities }
3243
3270
  }, args.log);
3244
3271
  }
3245
3272
  async function publishMessageStateForPortForwardResult(args, state, request, status, failedReason = "port_forward_denied") {
3246
3273
  if (state.kandanThreadId === undefined) {
3247
3274
  return;
3248
3275
  }
3276
+ const previousProcessingState = state.portForwardPreviousProcessingStates.get(request.requestId);
3277
+ state.portForwardPreviousProcessingStates.delete(request.requestId);
3249
3278
  const activeProcessingState = state.activeProcessingState;
3250
3279
  if (activeProcessingState !== undefined && activeProcessingState.seq === request.sourceSeq) {
3251
- await publishMessageState(args, state.kandanThreadId, request.sourceSeq, processingMessageStateFromActive(activeProcessingState), undefined, undefined, state.codexThreadId);
3252
- return;
3280
+ const resolvingActiveApproval = activeProcessingState.reason === "awaiting approval" && activeProcessingState.approval.requestId === request.requestId;
3281
+ if (resolvingActiveApproval) {
3282
+ state.activeProcessingState = undefined;
3283
+ if (previousProcessingState !== undefined && previousProcessingState.seq === request.sourceSeq) {
3284
+ state.activeProcessingState = previousProcessingState;
3285
+ await publishMessageState(args, state.kandanThreadId, request.sourceSeq, processingMessageStateFromActive(previousProcessingState), undefined, undefined, state.codexThreadId);
3286
+ return;
3287
+ }
3288
+ switch (state.turn.status) {
3289
+ case "active":
3290
+ case "completing":
3291
+ if (state.turn.queuedSeq === request.sourceSeq) {
3292
+ state.activeProcessingState = {
3293
+ seq: request.sourceSeq,
3294
+ reason: "running terminal command"
3295
+ };
3296
+ await publishMessageState(args, state.kandanThreadId, request.sourceSeq, {
3297
+ status: "processing",
3298
+ reason: "running terminal command"
3299
+ });
3300
+ return;
3301
+ }
3302
+ break;
3303
+ case "idle":
3304
+ case "starting":
3305
+ break;
3306
+ }
3307
+ }
3308
+ if (!resolvingActiveApproval) {
3309
+ await publishMessageState(args, state.kandanThreadId, request.sourceSeq, processingMessageStateFromActive(activeProcessingState), undefined, undefined, state.codexThreadId);
3310
+ return;
3311
+ }
3253
3312
  }
3254
3313
  switch (status) {
3255
3314
  case "processed":
@@ -3419,7 +3478,7 @@ function kandanMessageClaimKey(args, threadId, seq) {
3419
3478
  function claimKandanMessage(args, state, event) {
3420
3479
  const threadId = event.threadId;
3421
3480
  if (threadId === undefined) {
3422
- return false;
3481
+ return true;
3423
3482
  }
3424
3483
  const key = kandanMessageClaimKey(args, threadId, event.seq);
3425
3484
  if (claimedKandanMessageKeys.has(key)) {
@@ -3428,8 +3487,9 @@ function claimKandanMessage(args, state, event) {
3428
3487
  claimedKandanMessageKeys.add(key);
3429
3488
  state.claimedKandanMessageKeys.add(key);
3430
3489
  if (state.claimedKandanMessageKeys.size > maxClaimedKandanMessageKeys) {
3431
- const [expiredKey] = state.claimedKandanMessageKeys;
3432
- if (expiredKey !== undefined) {
3490
+ const overflow = state.claimedKandanMessageKeys.size - maxClaimedKandanMessageKeys;
3491
+ const expiredKeys = Array.from(state.claimedKandanMessageKeys).slice(0, overflow);
3492
+ for (const expiredKey of expiredKeys) {
3433
3493
  state.claimedKandanMessageKeys.delete(expiredKey);
3434
3494
  claimedKandanMessageKeys.delete(expiredKey);
3435
3495
  }
@@ -3604,7 +3664,16 @@ async function drainKandanMessageQueue(args, state, payloadContext) {
3604
3664
  new_codex_thread_id: newCodexThreadId
3605
3665
  });
3606
3666
  await postCodexThreadReboundMessage(args, state, payloadContext, oldCodexThreadId, newCodexThreadId);
3607
- state.pendingReconnectContextInjection = await fetchReconnectContextInjection(args, state);
3667
+ try {
3668
+ state.pendingReconnectContextInjection = await fetchReconnectContextInjection(args, state);
3669
+ } catch (contextError) {
3670
+ state.pendingReconnectContextInjection = undefined;
3671
+ args.log("codex.thread_reconnect_context_failed", {
3672
+ kandan_thread_id: state.kandanThreadId,
3673
+ codex_thread_id: newCodexThreadId,
3674
+ message: contextError instanceof Error ? contextError.message : String(contextError)
3675
+ });
3676
+ }
3608
3677
  requeuePendingKandanMessageFront(state.queue, next);
3609
3678
  state.turn = { status: "idle" };
3610
3679
  await drainKandanMessageQueue(args, state, payloadContext);
@@ -4963,6 +5032,9 @@ async function refreshActiveProcessingState(args, state, turnId, reason) {
4963
5032
  if (state.activeProcessingState?.seq === seq && state.activeProcessingState.reason === reason) {
4964
5033
  return;
4965
5034
  }
5035
+ if (state.activeProcessingState?.seq === seq && state.activeProcessingState.reason === "awaiting approval") {
5036
+ return;
5037
+ }
4966
5038
  state.activeProcessingState = { seq, reason };
4967
5039
  await publishMessageState(args, state.kandanThreadId, seq, {
4968
5040
  status: "processing",
@@ -4974,6 +5046,14 @@ async function refreshActiveProcessingHeartbeat(args, state) {
4974
5046
  if (activeProcessingState === undefined || state.kandanThreadId === undefined) {
4975
5047
  return;
4976
5048
  }
5049
+ if (activeProcessingState.reason === "awaiting approval") {
5050
+ switch (activeProcessingState.approval.kind) {
5051
+ case "local_runner_port_forward":
5052
+ return;
5053
+ default:
5054
+ break;
5055
+ }
5056
+ }
4977
5057
  await publishMessageState(args, state.kandanThreadId, activeProcessingState.seq, processingMessageStateFromActive(activeProcessingState), undefined, undefined, state.codexThreadId);
4978
5058
  }
4979
5059
  function clearActiveProcessingState(state, seq) {
@@ -5936,6 +6016,7 @@ function pathLooksAbsolute(pathValue) {
5936
6016
 
5937
6017
  // src/localForwarding.ts
5938
6018
  import { gzipSync } from "node:zlib";
6019
+ import NodeWebSocket from "ws";
5939
6020
  var maxForwardBodyBytes = 64 * 1024 * 1024;
5940
6021
  var gzipForwardThresholdBytes = 32 * 1024;
5941
6022
  async function handleForwardHttpRequest(control, allowedPorts) {
@@ -5979,15 +6060,15 @@ function isForwardHttpRequestControl(control) {
5979
6060
  function isForwardWebSocketControl(control) {
5980
6061
  return control.type === "forward_websocket_open" || control.type === "forward_websocket_send" || control.type === "forward_websocket_close";
5981
6062
  }
5982
- function createForwardWebSocketManager(kandan, topic, allowedPorts) {
6063
+ function createForwardWebSocketManager(kandan, topic, allowedPorts, socketFactory = defaultForwardWebSocketFactory) {
5983
6064
  const sockets = new Map;
5984
6065
  const pushEvent = (payload) => kandan.push(topic, "forward:websocket_event", payload).catch(() => {
5985
6066
  return;
5986
6067
  });
5987
6068
  const closeSocket = (socketId) => {
5988
- const socket = sockets.get(socketId);
6069
+ const stream = sockets.get(socketId);
5989
6070
  sockets.delete(socketId);
5990
- socket?.close();
6071
+ stream?.socket.close();
5991
6072
  };
5992
6073
  return {
5993
6074
  handle: (control) => {
@@ -6001,12 +6082,12 @@ function createForwardWebSocketManager(kandan, topic, allowedPorts) {
6001
6082
  });
6002
6083
  return;
6003
6084
  }
6004
- openLocalWebSocket(control, sockets, pushEvent, "ws");
6085
+ openLocalWebSocket(control, sockets, pushEvent, socketFactory, "ws");
6005
6086
  return;
6006
6087
  }
6007
6088
  case "forward_websocket_send": {
6008
- const socket = sockets.get(control.socketId);
6009
- if (socket === undefined || socket.readyState !== WebSocket.OPEN) {
6089
+ const stream = sockets.get(control.socketId);
6090
+ if (stream === undefined || stream.socket.readyState !== WebSocket.OPEN) {
6010
6091
  pushEvent({
6011
6092
  socketId: control.socketId,
6012
6093
  type: "error",
@@ -6017,12 +6098,12 @@ function createForwardWebSocketManager(kandan, topic, allowedPorts) {
6017
6098
  const body = Buffer.from(control.bodyBase64, "base64");
6018
6099
  switch (control.opcode) {
6019
6100
  case "text":
6020
- socket.send(body.toString());
6101
+ stream.socket.send(body.toString());
6021
6102
  return;
6022
6103
  case "binary":
6023
6104
  case "ping":
6024
6105
  case "pong":
6025
- socket.send(body);
6106
+ stream.socket.send(body);
6026
6107
  return;
6027
6108
  }
6028
6109
  }
@@ -6038,18 +6119,27 @@ function createForwardWebSocketManager(kandan, topic, allowedPorts) {
6038
6119
  }
6039
6120
  };
6040
6121
  }
6041
- function openLocalWebSocket(control, sockets, pushEvent, scheme) {
6122
+ function openLocalWebSocket(control, sockets, pushEvent, socketFactory, scheme) {
6042
6123
  let opened = false;
6043
- const url = localForwardUrl(scheme === "ws" ? "http" : "https", control.port, control.path, control.queryString).replace(/^http/, scheme);
6124
+ const url = localForwardWebSocketUrl(scheme, control.port, control.path, control.queryString);
6044
6125
  const protocols = webSocketProtocols(control.headers);
6045
- const websocket = protocols === undefined ? new WebSocket(url) : new WebSocket(url, protocols);
6126
+ const headers = webSocketHeaders(control.headers);
6127
+ const websocket = socketFactory(url, protocols, headers);
6046
6128
  websocket.binaryType = "arraybuffer";
6047
- sockets.set(control.socketId, websocket);
6129
+ const previousStream = sockets.get(control.socketId);
6130
+ previousStream?.socket.close();
6131
+ sockets.set(control.socketId, { socket: websocket });
6048
6132
  websocket.addEventListener("open", () => {
6133
+ if (!currentWebSocket(sockets, control.socketId, websocket)) {
6134
+ return;
6135
+ }
6049
6136
  opened = true;
6050
6137
  pushEvent({ socketId: control.socketId, type: "open" });
6051
6138
  });
6052
6139
  websocket.addEventListener("message", (event) => {
6140
+ if (!currentWebSocket(sockets, control.socketId, websocket)) {
6141
+ return;
6142
+ }
6053
6143
  const body = typeof event.data === "string" ? Buffer.from(event.data) : Buffer.from(event.data);
6054
6144
  pushEvent({
6055
6145
  socketId: control.socketId,
@@ -6059,6 +6149,9 @@ function openLocalWebSocket(control, sockets, pushEvent, scheme) {
6059
6149
  });
6060
6150
  });
6061
6151
  websocket.addEventListener("close", (event) => {
6152
+ if (!currentWebSocket(sockets, control.socketId, websocket)) {
6153
+ return;
6154
+ }
6062
6155
  sockets.delete(control.socketId);
6063
6156
  pushEvent({
6064
6157
  socketId: control.socketId,
@@ -6068,18 +6161,32 @@ function openLocalWebSocket(control, sockets, pushEvent, scheme) {
6068
6161
  });
6069
6162
  });
6070
6163
  websocket.addEventListener("error", () => {
6164
+ if (!currentWebSocket(sockets, control.socketId, websocket)) {
6165
+ return;
6166
+ }
6071
6167
  sockets.delete(control.socketId);
6072
6168
  if (!opened && scheme === "ws") {
6073
- openLocalWebSocket(control, sockets, pushEvent, "wss");
6169
+ openLocalWebSocket(control, sockets, pushEvent, socketFactory, "wss");
6074
6170
  return;
6075
6171
  }
6076
6172
  pushEvent({
6077
6173
  socketId: control.socketId,
6078
6174
  type: "error",
6079
- error: "websocket_error"
6175
+ error: "websocket_error",
6176
+ attemptedScheme: scheme
6080
6177
  });
6081
6178
  });
6082
6179
  }
6180
+ function defaultForwardWebSocketFactory(url, protocols, headers) {
6181
+ if (headers === undefined) {
6182
+ return protocols === undefined ? new WebSocket(url) : new WebSocket(url, protocols);
6183
+ }
6184
+ const options = { headers };
6185
+ return protocols === undefined ? new NodeWebSocket(url, options) : new NodeWebSocket(url, protocols, options);
6186
+ }
6187
+ function currentWebSocket(sockets, socketId, socket) {
6188
+ return sockets.get(socketId)?.socket === socket;
6189
+ }
6083
6190
  function webSocketProtocols(headers) {
6084
6191
  if (!Array.isArray(headers)) {
6085
6192
  return;
@@ -6092,6 +6199,21 @@ function webSocketProtocols(headers) {
6092
6199
  });
6093
6200
  return protocols.length === 0 ? undefined : protocols;
6094
6201
  }
6202
+ function webSocketHeaders(headers) {
6203
+ if (!Array.isArray(headers)) {
6204
+ return;
6205
+ }
6206
+ const forwarded = headers.reduce((acc, header) => {
6207
+ if (isOctWebSocketHeader(header)) {
6208
+ acc[header.name] = header.value;
6209
+ }
6210
+ return acc;
6211
+ }, {});
6212
+ return Object.keys(forwarded).length === 0 ? undefined : forwarded;
6213
+ }
6214
+ function isOctWebSocketHeader(value) {
6215
+ return isHeader(value) && value.name.toLowerCase().startsWith("x-oct-");
6216
+ }
6095
6217
  function localForwardUrl(scheme, port, path, queryString) {
6096
6218
  const normalizedPath = path.startsWith("/") ? path : `/${path}`;
6097
6219
  const url = new URL(`${scheme}://127.0.0.1:${port}${normalizedPath}`);
@@ -6100,6 +6222,12 @@ function localForwardUrl(scheme, port, path, queryString) {
6100
6222
  }
6101
6223
  return url.toString();
6102
6224
  }
6225
+ function localForwardWebSocketUrl(scheme, port, path, queryString) {
6226
+ const httpScheme = scheme === "ws" ? "http" : "https";
6227
+ const url = new URL(localForwardUrl(httpScheme, port, path, queryString));
6228
+ url.protocol = `${scheme}:`;
6229
+ return url.toString();
6230
+ }
6103
6231
  async function fetchWithHttpsFallback(port, path, queryString, request) {
6104
6232
  try {
6105
6233
  return await fetch(localForwardUrl("http", port, path, queryString), request);
@@ -6212,8 +6340,10 @@ var blockedForwardHeaderNames = new Set([
6212
6340
  "connection",
6213
6341
  "content-encoding",
6214
6342
  "content-length",
6343
+ "cookie",
6215
6344
  "host",
6216
6345
  "keep-alive",
6346
+ "authorization",
6217
6347
  "proxy-authenticate",
6218
6348
  "proxy-authorization",
6219
6349
  "te",
@@ -6225,10 +6355,12 @@ var blockedForwardHeaderNames = new Set([
6225
6355
  // src/localEditor.ts
6226
6356
  import { spawn as spawn2 } from "node:child_process";
6227
6357
  import {
6358
+ copyFileSync,
6228
6359
  cpSync,
6229
6360
  existsSync as existsSync2,
6230
6361
  mkdirSync as mkdirSync3,
6231
6362
  mkdtempSync,
6363
+ readFileSync as readFileSync2,
6232
6364
  realpathSync as realpathSync3,
6233
6365
  writeFileSync as writeFileSync2
6234
6366
  } from "node:fs";
@@ -6430,6 +6562,7 @@ function prepareCodeServerProfile(collaboration, editorRuntime) {
6430
6562
  mkdirSync3(collaborationServerDir, { recursive: true });
6431
6563
  mkdirSync3(tempDir, { recursive: true });
6432
6564
  if (editorRuntime !== undefined) {
6565
+ ensureCodeServerBrowserExtensionAssets(editorRuntime);
6433
6566
  installDirectory(editorRuntime.assets.documentStateExtensionDir, join4(extensionsDir, "kandan.document-state-telemetry"));
6434
6567
  }
6435
6568
  writeFileSync2(join4(userSettingsDir, "settings.json"), JSON.stringify(codeServerSettings(collaboration), null, 2));
@@ -6438,6 +6571,68 @@ function prepareCodeServerProfile(collaboration, editorRuntime) {
6438
6571
  return { ok: false, reason: "code_server_spawn_failed" };
6439
6572
  }
6440
6573
  }
6574
+ function ensureCodeServerBrowserExtensionAssets(runtime) {
6575
+ const vscodeRoot = codeServerVscodeRoot(runtime);
6576
+ const repairs = [
6577
+ {
6578
+ source: join4(vscodeRoot, "extensions", "git-base", "dist", "extension.js"),
6579
+ target: join4(vscodeRoot, "extensions", "git-base", "dist", "browser", "extension.js"),
6580
+ required: true
6581
+ },
6582
+ {
6583
+ source: join4(vscodeRoot, "extensions", "git-base", "dist", "extension.js.map"),
6584
+ target: join4(vscodeRoot, "extensions", "git-base", "dist", "browser", "extension.js.map"),
6585
+ required: false
6586
+ },
6587
+ {
6588
+ source: join4(vscodeRoot, "extensions", "merge-conflict", "dist", "mergeConflictMain.js"),
6589
+ target: join4(vscodeRoot, "extensions", "merge-conflict", "dist", "browser", "mergeConflictMain.js"),
6590
+ required: true
6591
+ },
6592
+ {
6593
+ source: join4(vscodeRoot, "extensions", "merge-conflict", "dist", "mergeConflictMain.js.map"),
6594
+ target: join4(vscodeRoot, "extensions", "merge-conflict", "dist", "browser", "mergeConflictMain.js.map"),
6595
+ required: false
6596
+ }
6597
+ ];
6598
+ repairs.forEach(({ source, target, required }) => {
6599
+ switch (true) {
6600
+ case existsSync2(target):
6601
+ return;
6602
+ case (!required && !existsSync2(source)):
6603
+ return;
6604
+ default:
6605
+ mkdirSync3(dirname2(target), { recursive: true });
6606
+ copyFileSync(source, target);
6607
+ return;
6608
+ }
6609
+ });
6610
+ }
6611
+ function codeServerVscodeRoot(runtime) {
6612
+ const roots = uniquePaths([
6613
+ join4(runtime.root, "lib", "vscode"),
6614
+ join4(dirname2(dirname2(runtime.codeServerBin)), "lib", "vscode"),
6615
+ ...wrappedCodeServerBin(runtime.codeServerBin).map((codeServerBin) => join4(dirname2(dirname2(codeServerBin)), "lib", "vscode"))
6616
+ ]);
6617
+ const rootWithRequiredAssets = roots.find((root) => codeServerBrowserAssetSources(root).every((source) => existsSync2(source)));
6618
+ return rootWithRequiredAssets ?? roots[0];
6619
+ }
6620
+ function wrappedCodeServerBin(codeServerBin) {
6621
+ try {
6622
+ const script = readFileSync2(codeServerBin, "utf8");
6623
+ const match = script.match(/exec\s+(?:"([^"]+)"|'([^']+)'|([^\s]+))\s+"\$@"/u);
6624
+ const target = match?.[1] ?? match?.[2] ?? match?.[3];
6625
+ return target === undefined || target.trim() === "" ? [] : [target];
6626
+ } catch (_error) {
6627
+ return [];
6628
+ }
6629
+ }
6630
+ function codeServerBrowserAssetSources(vscodeRoot) {
6631
+ return [
6632
+ join4(vscodeRoot, "extensions", "git-base", "dist", "extension.js"),
6633
+ join4(vscodeRoot, "extensions", "merge-conflict", "dist", "mergeConflictMain.js")
6634
+ ];
6635
+ }
6441
6636
  function prepareCodeServerLaunch(options) {
6442
6637
  const platform = options.platform ?? process.platform;
6443
6638
  if (platform === "linux") {
@@ -6570,12 +6765,16 @@ function prepareLocalEditorCollaboration(collaboration, runnerId, serverPort, br
6570
6765
  }
6571
6766
  const targetPath = `/local-codex-runners/${encodeURIComponent(runnerId)}/forwards/${serverPort}/preview-target`;
6572
6767
  const serverUrl = new URL(targetPath, `${browserBaseUrl}/`).toString();
6573
- const bootstrapServerUrl = collaboration.bootstrapToken === undefined || collaboration.bootstrapToken === "" ? serverUrl : new URL(`${targetPath}/_kandan-collaboration/${encodeURIComponent(collaboration.bootstrapToken)}`, `${browserBaseUrl}/`).toString();
6768
+ const collaborationStatePath = `/api/v2/editor/sessions/${collaboration.editorSessionId}/collaboration-state`;
6769
+ const collaborationBootstrapPath = collaboration.bootstrapToken === undefined || collaboration.bootstrapToken === "" ? undefined : `${targetPath}/_kandan-collaboration/${encodeURIComponent(collaboration.bootstrapToken)}`;
6770
+ const collaborationStateUrl = collaborationBootstrapPath === undefined ? collaborationStatePath : `${collaborationBootstrapPath}${collaborationStatePath}`;
6771
+ const bootstrapServerUrl = collaborationBootstrapPath === undefined ? serverUrl : new URL(collaborationBootstrapPath, `${browserBaseUrl}/`).toString();
6574
6772
  return {
6575
6773
  ...collaboration,
6576
6774
  serverPort,
6577
6775
  serverUrl,
6578
- bootstrapServerUrl
6776
+ bootstrapServerUrl,
6777
+ collaborationStateUrl
6579
6778
  };
6580
6779
  }
6581
6780
  function codeServerSettings(collaboration) {
@@ -6588,6 +6787,7 @@ function codeServerSettings(collaboration) {
6588
6787
  "oct.joinAcceptMode": "auto",
6589
6788
  ...collaboration === undefined ? {} : {
6590
6789
  "oct.kandanEditorSessionId": collaboration.editorSessionId,
6790
+ "oct.kandanCollaborationStateUrl": collaboration.collaborationStateUrl,
6591
6791
  "oct.serverUrl": collaboration.bootstrapServerUrl
6592
6792
  },
6593
6793
  "security.workspace.trust.enabled": false,
@@ -6666,6 +6866,7 @@ async function startCollaborationSidecar(collaboration, profile, editorRuntime,
6666
6866
  serverPort: collaboration.serverPort,
6667
6867
  serverUrl: collaboration.serverUrl,
6668
6868
  bootstrapServerUrl: collaboration.bootstrapServerUrl,
6869
+ collaborationStateUrl: collaboration.collaborationStateUrl,
6669
6870
  process: child,
6670
6871
  exited
6671
6872
  }
@@ -6906,7 +7107,7 @@ import {
6906
7107
  existsSync as existsSync3,
6907
7108
  mkdirSync as mkdirSync4,
6908
7109
  mkdtempSync as mkdtempSync2,
6909
- readFileSync as readFileSync2,
7110
+ readFileSync as readFileSync3,
6910
7111
  renameSync,
6911
7112
  rmSync,
6912
7113
  writeFileSync as writeFileSync3
@@ -7394,7 +7595,7 @@ function installedRuntime(cacheRoot, manifest) {
7394
7595
  return { ok: false };
7395
7596
  }
7396
7597
  try {
7397
- const installed = JSON.parse(readFileSync2(manifestPath, "utf8"));
7598
+ const installed = JSON.parse(readFileSync3(manifestPath, "utf8"));
7398
7599
  if (isJsonObject(installed) && installed.version === manifest.version && installed.platform === manifest.platform && (installed.archiveSha256 === undefined || installed.archiveSha256 === manifest.archiveSha256)) {
7399
7600
  return {
7400
7601
  ok: true,
@@ -7625,7 +7826,7 @@ function manifestAssetChecksums(assets) {
7625
7826
  return checksums;
7626
7827
  }
7627
7828
  function fileSha256Sync(path) {
7628
- return createHash2("sha256").update(readFileSync2(path)).digest("hex");
7829
+ return createHash2("sha256").update(readFileSync3(path)).digest("hex");
7629
7830
  }
7630
7831
  function defaultEditorRuntimeCacheRoot() {
7631
7832
  return join5(homedir4(), ".linzumi", "editor-runtimes");
@@ -7804,6 +8005,7 @@ function firstNonBlank(...values) {
7804
8005
  }
7805
8006
 
7806
8007
  // src/phoenix.ts
8008
+ var defaultPushTimeoutMs = 30000;
7807
8009
  function phoenixWebsocketUrl(baseUrl, token) {
7808
8010
  const parsed = new URL(baseUrl);
7809
8011
  switch (parsed.protocol) {
@@ -7819,7 +8021,7 @@ function phoenixWebsocketUrl(baseUrl, token) {
7819
8021
  parsed.searchParams.set("vsn", "2.0.0");
7820
8022
  return parsed.toString();
7821
8023
  }
7822
- async function connectPhoenixClient(baseUrl, token, socketFactory = (url) => new WebSocket(url)) {
8024
+ async function connectPhoenixClient(baseUrl, token, socketFactory = (url) => new WebSocket(url), options = {}) {
7823
8025
  const pending = new Map;
7824
8026
  const joins = new Map;
7825
8027
  const controlCallbacks = new Set;
@@ -7837,9 +8039,13 @@ async function connectPhoenixClient(baseUrl, token, socketFactory = (url) => new
7837
8039
  rejectReady: undefined,
7838
8040
  reconnectTimer: undefined
7839
8041
  };
8042
+ const pushTimeoutMs = options.pushTimeoutMs ?? defaultPushTimeoutMs;
7840
8043
  const rejectPending = (message) => {
7841
8044
  const error = new Error(message);
7842
- pending.forEach((pendingPush) => pendingPush.reject(error));
8045
+ pending.forEach((pendingPush) => {
8046
+ clearTimeout(pendingPush.timer);
8047
+ pendingPush.reject(error);
8048
+ });
7843
8049
  pending.clear();
7844
8050
  };
7845
8051
  const resetReady = () => {
@@ -7855,6 +8061,7 @@ async function connectPhoenixClient(baseUrl, token, socketFactory = (url) => new
7855
8061
  const pendingPush = pending.get(ref);
7856
8062
  if (pendingPush !== undefined) {
7857
8063
  pending.delete(ref);
8064
+ clearTimeout(pendingPush.timer);
7858
8065
  if (name === "phx_error") {
7859
8066
  pendingPush.reject(new Error("phoenix push failed"));
7860
8067
  } else if (isNonOkPushReply(payload) && pendingPush.event !== "phx_join") {
@@ -7880,7 +8087,12 @@ async function connectPhoenixClient(baseUrl, token, socketFactory = (url) => new
7880
8087
  state.nextRef += 1;
7881
8088
  const frame = [null, ref, topic, event, payload];
7882
8089
  return new Promise((resolve5, reject) => {
7883
- pending.set(ref, { event, resolve: resolve5, reject });
8090
+ const timer = setTimeout(() => {
8091
+ if (pending.delete(ref)) {
8092
+ reject(new Error(`phoenix push timed out after ${pushTimeoutMs}ms: ${event}`));
8093
+ }
8094
+ }, pushTimeoutMs);
8095
+ pending.set(ref, { event, timer, resolve: resolve5, reject });
7884
8096
  websocket.send(JSON.stringify(frame));
7885
8097
  });
7886
8098
  };
@@ -7955,10 +8167,10 @@ async function connectPhoenixClient(baseUrl, token, socketFactory = (url) => new
7955
8167
  return pushOnOpenSocket(topic, event, payload);
7956
8168
  };
7957
8169
  return {
7958
- join: async (topic, payload, options) => {
8170
+ join: async (topic, payload, options2) => {
7959
8171
  const reply = await push(topic, "phx_join", payload);
7960
8172
  if (isJoinReply(reply)) {
7961
- joins.set(topic, { payload: options?.rejoinPayload ?? (() => payload) });
8173
+ joins.set(topic, { payload: options2?.rejoinPayload ?? (() => payload) });
7962
8174
  return reply.response;
7963
8175
  }
7964
8176
  throw new Error(`phoenix join failed: ${joinErrorMessage(reply)}`);
@@ -8030,7 +8242,7 @@ import {
8030
8242
  existsSync as existsSync5,
8031
8243
  mkdirSync as mkdirSync6,
8032
8244
  openSync as openSync2,
8033
- readFileSync as readFileSync4,
8245
+ readFileSync as readFileSync5,
8034
8246
  unlinkSync as unlinkSync2,
8035
8247
  writeSync
8036
8248
  } from "node:fs";
@@ -8042,27 +8254,38 @@ import {
8042
8254
  existsSync as existsSync4,
8043
8255
  linkSync,
8044
8256
  mkdirSync as mkdirSync5,
8045
- readFileSync as readFileSync3,
8257
+ readFileSync as readFileSync4,
8046
8258
  realpathSync as realpathSync4,
8047
8259
  unlinkSync,
8048
8260
  writeFileSync as writeFileSync4
8049
8261
  } from "node:fs";
8050
8262
  import { homedir as homedir5 } from "node:os";
8051
8263
  import { basename as basename4, dirname as dirname4, join as join6, resolve as resolve5 } from "node:path";
8264
+
8265
+ // src/defaultUrls.ts
8266
+ var defaultLinzumiHttpUrl = "https://serve.linzumi.com";
8267
+ var defaultLinzumiWebSocketUrl = "wss://serve.linzumi.com";
8268
+
8269
+ // src/localConfig.ts
8270
+ var prodConfigScope = "prod";
8052
8271
  function localConfigPath(env = process.env) {
8053
8272
  const override = env.LINZUMI_CONFIG_FILE;
8054
8273
  return override !== undefined && override.trim() !== "" ? resolve5(expandUserPath(override)) : resolve5(homedir5(), ".linzumi", "config.json");
8055
8274
  }
8275
+ function localConfigScopeKey(linzumiUrl) {
8276
+ const normalizedUrl = kandanHttpBaseUrl(linzumiUrl);
8277
+ const normalizedProdUrl = kandanHttpBaseUrl(defaultLinzumiWebSocketUrl);
8278
+ return normalizedUrl === normalizedProdUrl ? prodConfigScope : normalizedUrl;
8279
+ }
8280
+ function localConfigScopeFileStem(linzumiUrl) {
8281
+ const scopeKey = localConfigScopeKey(linzumiUrl);
8282
+ return scopeKey === prodConfigScope ? prodConfigScope : Buffer.from(scopeKey).toString("base64url");
8283
+ }
8056
8284
  function readLocalConfig(path = localConfigPath()) {
8057
- if (!existsSync4(path)) {
8058
- return { version: 1, allowedCwds: [] };
8059
- }
8060
- const parsed = JSON.parse(readFileSync3(path, "utf8"));
8061
- if (!isConfigPayload(parsed)) {
8062
- throw new Error(`invalid Linzumi config: ${path}`);
8063
- }
8064
- const allowedCwds = uniqueStrings(parsed.allowedCwds);
8065
- return parsed.machineId === undefined ? { version: 1, allowedCwds } : { version: 1, machineId: parsed.machineId, allowedCwds };
8285
+ return readLocalConfigSection(path);
8286
+ }
8287
+ function readLocalConfigForLinzumiUrl(linzumiUrl, path = localConfigPath()) {
8288
+ return readLocalConfigSection(path, linzumiUrl);
8066
8289
  }
8067
8290
  function ensureLocalMachineId(path = localConfigPath(), createMachineId = randomUUID2) {
8068
8291
  const config = readLocalConfig(path);
@@ -8078,13 +8301,36 @@ function ensureLocalMachineId(path = localConfigPath(), createMachineId = random
8078
8301
  writeLocalConfig({ ...latestConfig, machineId }, path);
8079
8302
  return machineId;
8080
8303
  }
8081
- function localMachineIdSeedPath(configPath = localConfigPath()) {
8304
+ function ensureLocalMachineIdForLinzumiUrl(linzumiUrl, path = localConfigPath(), createMachineId = randomUUID2) {
8305
+ if (localConfigScopeKey(linzumiUrl) === prodConfigScope) {
8306
+ return ensureLocalMachineId(path, createMachineId);
8307
+ }
8308
+ const config = readLocalConfigForLinzumiUrl(linzumiUrl, path);
8309
+ if (config.machineId !== undefined) {
8310
+ return config.machineId;
8311
+ }
8312
+ const machineId = ensureLocalMachineIdSeed(path, createMachineId, linzumiUrl);
8313
+ const latestConfig = readLocalConfigForLinzumiUrl(linzumiUrl, path);
8314
+ const latestMachineId = latestConfig.machineId;
8315
+ if (latestMachineId !== undefined) {
8316
+ return latestMachineId;
8317
+ }
8318
+ writeLocalConfigSection({ ...latestConfig, machineId }, path, linzumiUrl);
8319
+ return machineId;
8320
+ }
8321
+ function localMachineIdSeedPath(configPath = localConfigPath(), linzumiUrl) {
8322
+ if (linzumiUrl !== undefined && localConfigScopeKey(linzumiUrl) !== prodConfigScope) {
8323
+ return join6(dirname4(configPath), `${basename4(configPath)}.${localConfigScopeFileStem(linzumiUrl)}.machine-id`);
8324
+ }
8082
8325
  return join6(dirname4(configPath), `${basename4(configPath)}.machine-id`);
8083
8326
  }
8084
- function readConfiguredAllowedCwdDetails(path = localConfigPath()) {
8327
+ function readConfiguredAllowedCwdDetailsForLinzumiUrl(linzumiUrl, path = localConfigPath()) {
8328
+ return readConfiguredAllowedCwdDetailsFromConfig(readLocalConfigForLinzumiUrl(linzumiUrl, path));
8329
+ }
8330
+ function readConfiguredAllowedCwdDetailsFromConfig(config) {
8085
8331
  const allowedCwds = [];
8086
8332
  const missingAllowedCwds = [];
8087
- for (const cwd of readLocalConfig(path).allowedCwds) {
8333
+ for (const cwd of config.allowedCwds) {
8088
8334
  const absolutePath = resolve5(expandUserPath(cwd));
8089
8335
  try {
8090
8336
  const realPath = realpathSync4(absolutePath);
@@ -8103,36 +8349,91 @@ function readConfiguredAllowedCwdDetails(path = localConfigPath()) {
8103
8349
  };
8104
8350
  }
8105
8351
  function addAllowedCwd(pathValue, path = localConfigPath()) {
8352
+ return addAllowedCwdToConfig(pathValue, path);
8353
+ }
8354
+ function addAllowedCwdForLinzumiUrl(pathValue, linzumiUrl, path = localConfigPath()) {
8355
+ return addAllowedCwdToConfig(pathValue, path, linzumiUrl);
8356
+ }
8357
+ function addAllowedCwdToConfig(pathValue, path, linzumiUrl) {
8106
8358
  const normalizedPath = realpathSync4(resolve5(expandUserPath(pathValue)));
8107
- const config = readLocalConfig(path);
8359
+ const config = readLocalConfigSection(path, linzumiUrl);
8108
8360
  const allowedCwds = uniqueStrings([...config.allowedCwds, normalizedPath]);
8109
- writeLocalConfig({ ...config, version: 1, allowedCwds }, path);
8361
+ writeLocalConfigSection({ ...config, version: 1, allowedCwds }, path, linzumiUrl);
8110
8362
  return allowedCwds;
8111
8363
  }
8112
8364
  function removeAllowedCwd(pathValue, path = localConfigPath()) {
8365
+ return removeAllowedCwdFromConfig(pathValue, path);
8366
+ }
8367
+ function removeAllowedCwdForLinzumiUrl(pathValue, linzumiUrl, path = localConfigPath()) {
8368
+ return removeAllowedCwdFromConfig(pathValue, path, linzumiUrl);
8369
+ }
8370
+ function removeAllowedCwdFromConfig(pathValue, path, linzumiUrl) {
8113
8371
  const requestedPath = resolve5(expandUserPath(pathValue));
8114
8372
  const normalizedRequest = realpathOrResolved(requestedPath);
8115
- const config = readLocalConfig(path);
8373
+ const config = readLocalConfigSection(path, linzumiUrl);
8116
8374
  const allowedCwds = config.allowedCwds.filter((cwd) => {
8117
8375
  const normalizedExisting = realpathOrResolved(cwd);
8118
8376
  return cwd !== pathValue && normalizedExisting !== normalizedRequest;
8119
8377
  });
8120
- writeLocalConfig({ ...config, version: 1, allowedCwds }, path);
8378
+ writeLocalConfigSection({ ...config, version: 1, allowedCwds }, path, linzumiUrl);
8121
8379
  return allowedCwds;
8122
8380
  }
8123
8381
  function writeLocalConfig(config, path = localConfigPath()) {
8382
+ writeLocalConfigSection(config, path);
8383
+ }
8384
+ function readLocalConfigFile(path) {
8385
+ if (!existsSync4(path)) {
8386
+ return { version: 1, allowedCwds: [] };
8387
+ }
8388
+ const parsed = JSON.parse(readFileSync4(path, "utf8"));
8389
+ if (!isConfigPayload(parsed)) {
8390
+ throw new Error(`invalid Linzumi config: ${path}`);
8391
+ }
8392
+ return parsed;
8393
+ }
8394
+ function readLocalConfigSection(path, linzumiUrl) {
8395
+ const parsed = readLocalConfigFile(path);
8396
+ const scopeKey = linzumiUrl === undefined ? prodConfigScope : localConfigScopeKey(linzumiUrl);
8397
+ const section = scopeKey === prodConfigScope ? parsed : parsed[scopeKey] ?? emptySection();
8398
+ if (!isConfigSection(section)) {
8399
+ throw new Error(`invalid Linzumi config section ${scopeKey}: ${path}`);
8400
+ }
8401
+ const allowedCwds = uniqueStrings(section.allowedCwds);
8402
+ return section.machineId === undefined ? { version: 1, allowedCwds } : { version: 1, machineId: section.machineId, allowedCwds };
8403
+ }
8404
+ function writeLocalConfigSection(config, path, linzumiUrl) {
8405
+ const scopeKey = linzumiUrl === undefined ? prodConfigScope : localConfigScopeKey(linzumiUrl);
8406
+ const nextSection = normalizedConfigSection(config);
8407
+ const next = scopeKey === prodConfigScope ? { ...readLocalConfigFile(path), ...nextSection, version: 1 } : {
8408
+ ...readLocalConfigFile(path),
8409
+ version: 1,
8410
+ [scopeKey]: nextSection
8411
+ };
8124
8412
  mkdirSync5(dirname4(path), { recursive: true });
8125
- writeFileSync4(path, `${JSON.stringify(config, null, 2)}
8413
+ writeFileSync4(path, `${JSON.stringify(next, null, 2)}
8126
8414
  `, "utf8");
8127
8415
  }
8128
8416
  function isConfigPayload(value) {
8129
- return typeof value === "object" && value !== null && value.version === 1 && Array.isArray(value.allowedCwds) && machineIdValid(value.machineId) && value.allowedCwds.every((cwd) => typeof cwd === "string" && cwd.trim() !== "");
8417
+ return typeof value === "object" && value !== null && value.version === 1 && isConfigSection(value);
8418
+ }
8419
+ function isConfigSection(value) {
8420
+ return typeof value === "object" && value !== null && Array.isArray(value.allowedCwds) && machineIdValid(value.machineId) && value.allowedCwds.every((cwd) => typeof cwd === "string" && cwd.trim() !== "");
8421
+ }
8422
+ function normalizedConfigSection(config) {
8423
+ if (!isConfigPayload(config)) {
8424
+ throw new Error("invalid Linzumi config");
8425
+ }
8426
+ const allowedCwds = uniqueStrings(config.allowedCwds);
8427
+ return config.machineId === undefined ? { allowedCwds } : { machineId: config.machineId, allowedCwds };
8428
+ }
8429
+ function emptySection() {
8430
+ return { allowedCwds: [] };
8130
8431
  }
8131
8432
  function machineIdValid(value) {
8132
8433
  return value === undefined || typeof value === "string" && /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value);
8133
8434
  }
8134
- function ensureLocalMachineIdSeed(configPath, createMachineId) {
8135
- const seedPath = localMachineIdSeedPath(configPath);
8435
+ function ensureLocalMachineIdSeed(configPath, createMachineId, linzumiUrl) {
8436
+ const seedPath = localMachineIdSeedPath(configPath, linzumiUrl);
8136
8437
  if (existsSync4(seedPath)) {
8137
8438
  return readMachineIdSeed(seedPath);
8138
8439
  }
@@ -8157,7 +8458,7 @@ function ensureLocalMachineIdSeed(configPath, createMachineId) {
8157
8458
  }
8158
8459
  }
8159
8460
  function readMachineIdSeed(seedPath) {
8160
- const machineId = readFileSync3(seedPath, "utf8").trim();
8461
+ const machineId = readFileSync4(seedPath, "utf8").trim();
8161
8462
  if (!machineIdValid(machineId)) {
8162
8463
  throw new Error(`invalid Linzumi machine id seed: ${seedPath}`);
8163
8464
  }
@@ -8183,15 +8484,16 @@ function realpathOrResolved(pathValue) {
8183
8484
  }
8184
8485
 
8185
8486
  // src/version.ts
8186
- var linzumiCliVersion = "0.0.43-beta";
8487
+ var linzumiCliVersion = "0.0.45-beta";
8187
8488
  var linzumiCliVersionText = `linzumi ${linzumiCliVersion}`;
8188
8489
 
8189
8490
  // src/runnerLock.ts
8190
- function runnerLockPath(machineId, configPath = localConfigPath()) {
8191
- return join7(dirname5(configPath), "runners", `${encodeURIComponent(machineId)}.lock`);
8491
+ function runnerLockPath(machineId, configPath = localConfigPath(), linzumiUrl) {
8492
+ const lockName = linzumiUrl === undefined ? encodeURIComponent(machineId) : localConfigScopeFileStem(linzumiUrl);
8493
+ return join7(dirname5(configPath), "runners", `${lockName}.lock`);
8192
8494
  }
8193
8495
  function acquireRunnerLock(options) {
8194
- const path = runnerLockPath(options.machineId, options.configPath);
8496
+ const path = runnerLockPath(options.machineId, options.configPath, options.linzumiUrl);
8195
8497
  const isPidAlive = options.isPidAlive ?? processIsAlive;
8196
8498
  const record = {
8197
8499
  version: 1,
@@ -8200,9 +8502,11 @@ function acquireRunnerLock(options) {
8200
8502
  pid: options.pid ?? process.pid,
8201
8503
  cwd: options.cwd,
8202
8504
  workspace: options.workspace,
8505
+ ...options.linzumiUrl === undefined ? {} : { linzumiUrl: kandanHttpBaseUrl(options.linzumiUrl) },
8203
8506
  startedAt: (options.now ?? (() => new Date))().toISOString(),
8204
8507
  cliVersion: options.cliVersion ?? linzumiCliVersion
8205
8508
  };
8509
+ rejectLiveLegacyProductionRunnerLock(options.machineId, options.configPath, options.linzumiUrl, isPidAlive);
8206
8510
  writeLockOrHandleExisting(path, record, isPidAlive, options.beforeReadExistingLock, options.beforeReplaceStaleLock);
8207
8511
  return {
8208
8512
  path,
@@ -8210,6 +8514,28 @@ function acquireRunnerLock(options) {
8210
8514
  release: () => releaseRunnerLock(path, record)
8211
8515
  };
8212
8516
  }
8517
+ function rejectLiveLegacyProductionRunnerLock(machineId, configPath, linzumiUrl, isPidAlive) {
8518
+ if (linzumiUrl === undefined || localConfigScopeKey(linzumiUrl) !== localConfigScopeKey(defaultLinzumiHttpUrl)) {
8519
+ return;
8520
+ }
8521
+ const legacyPath = runnerLockPath(machineId, configPath);
8522
+ const existing = readRunnerLockIfPresent(legacyPath);
8523
+ if (existing === undefined) {
8524
+ return;
8525
+ }
8526
+ if (isPidAlive(existing.pid)) {
8527
+ throw new Error(activeRunnerLockMessage(legacyPath, existing));
8528
+ }
8529
+ withStaleReplacementLock(legacyPath, isPidAlive, () => {
8530
+ const latest = existsSync5(legacyPath) ? readRunnerLock(legacyPath) : undefined;
8531
+ if (latest !== undefined && isPidAlive(latest.pid)) {
8532
+ throw new Error(activeRunnerLockMessage(legacyPath, latest));
8533
+ }
8534
+ if (latest !== undefined) {
8535
+ unlinkSync2(legacyPath);
8536
+ }
8537
+ });
8538
+ }
8213
8539
  function writeLockOrHandleExisting(path, record, isPidAlive, beforeReadExistingLock, beforeReplaceStaleLock) {
8214
8540
  if (tryCreateLock(path, record)) {
8215
8541
  return;
@@ -8295,7 +8621,7 @@ function withStaleReplacementLock(path, isPidAlive, callback) {
8295
8621
  function readReplacementLockPidIfPresent(path) {
8296
8622
  let value;
8297
8623
  try {
8298
- value = readFileSync4(path, "utf8").trim();
8624
+ value = readFileSync5(path, "utf8").trim();
8299
8625
  } catch (error) {
8300
8626
  if (isNodeErrorCode2(error, "ENOENT")) {
8301
8627
  return;
@@ -8335,14 +8661,17 @@ function readRunnerLockIfPresent(path) {
8335
8661
  }
8336
8662
  }
8337
8663
  function readRunnerLock(path) {
8338
- const parsed = JSON.parse(readFileSync4(path, "utf8"));
8664
+ const parsed = JSON.parse(readFileSync5(path, "utf8"));
8339
8665
  if (!isRunnerLockRecord(parsed)) {
8340
8666
  throw new Error(`invalid Linzumi runner lock: ${path}`);
8341
8667
  }
8342
8668
  return parsed;
8343
8669
  }
8344
8670
  function isRunnerLockRecord(value) {
8345
- return typeof value === "object" && value !== null && value.version === 1 && typeof value.machineId === "string" && value.machineId.trim() !== "" && typeof value.runnerId === "string" && value.runnerId.trim() !== "" && Number.isInteger(value.pid) && value.pid > 0 && typeof value.cwd === "string" && value.cwd.trim() !== "" && workspaceValid(value.workspace) && typeof value.startedAt === "string" && value.startedAt.trim() !== "" && typeof value.cliVersion === "string" && value.cliVersion.trim() !== "";
8671
+ return typeof value === "object" && value !== null && value.version === 1 && typeof value.machineId === "string" && value.machineId.trim() !== "" && typeof value.runnerId === "string" && value.runnerId.trim() !== "" && Number.isInteger(value.pid) && value.pid > 0 && typeof value.cwd === "string" && value.cwd.trim() !== "" && workspaceValid(value.workspace) && linzumiUrlValid(value.linzumiUrl) && typeof value.startedAt === "string" && value.startedAt.trim() !== "" && typeof value.cliVersion === "string" && value.cliVersion.trim() !== "";
8672
+ }
8673
+ function linzumiUrlValid(value) {
8674
+ return value === undefined || typeof value === "string" && value.trim() !== "";
8346
8675
  }
8347
8676
  function workspaceValid(value) {
8348
8677
  return value === null || typeof value === "string" && value.trim() !== "";
@@ -8357,8 +8686,10 @@ function processIsAlive(pid) {
8357
8686
  }
8358
8687
  function activeRunnerLockMessage(path, record) {
8359
8688
  const workspace = record.workspace === null ? "workspace: unknown" : `workspace: ${record.workspace}`;
8689
+ const linzumiUrl = record.linzumiUrl === undefined ? undefined : `linzumi url: ${displayLinzumiUrl(record.linzumiUrl)}`;
8360
8690
  return [
8361
- "another Linzumi runner is already running for this machine",
8691
+ record.linzumiUrl === undefined ? "another Linzumi runner is already running for this machine" : "another Linzumi runner is already running for this Linzumi URL",
8692
+ ...linzumiUrl === undefined ? [] : [linzumiUrl],
8362
8693
  `runner id: ${record.runnerId}`,
8363
8694
  `pid: ${record.pid}`,
8364
8695
  `cwd: ${record.cwd}`,
@@ -8370,6 +8701,12 @@ function activeRunnerLockMessage(path, record) {
8370
8701
  ].join(`
8371
8702
  `);
8372
8703
  }
8704
+ function displayLinzumiUrl(linzumiUrl) {
8705
+ if (linzumiUrl === localConfigScopeKey(defaultLinzumiHttpUrl)) {
8706
+ return defaultLinzumiHttpUrl;
8707
+ }
8708
+ return localConfigScopeKey(linzumiUrl) === localConfigScopeKey(defaultLinzumiHttpUrl) ? defaultLinzumiHttpUrl : linzumiUrl;
8709
+ }
8373
8710
  function isNodeErrorCode2(error, code) {
8374
8711
  return typeof error === "object" && error !== null && "code" in error && error.code === code;
8375
8712
  }
@@ -8531,6 +8868,7 @@ async function runLocalCodexRunner(options) {
8531
8868
  runnerId: options.runnerId,
8532
8869
  cwd: options.cwd,
8533
8870
  workspace: runnerWorkspaceSlug(options) ?? null,
8871
+ linzumiUrl: options.kandanUrl,
8534
8872
  configPath: options.runnerLockConfigPath
8535
8873
  });
8536
8874
  cleanup.actions.push(() => runnerLock.release());
@@ -8552,6 +8890,8 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
8552
8890
  const allowedForwardPorts = options.allowedForwardPorts ?? [];
8553
8891
  const liveForwardPorts = new Set(allowedForwardPorts);
8554
8892
  const managedForwardPorts = new Set;
8893
+ const kandanControlPort = explicitUrlPort(options.kandanUrl);
8894
+ const suppressedForwardPorts = () => suppressedForwardPortsForRunner(kandanControlPort, managedForwardPorts);
8555
8895
  const forwardPortAttributions = new Map;
8556
8896
  const setForwardPortAttribution = (port, attribution) => {
8557
8897
  forwardPortAttributions.set(port, {
@@ -8559,7 +8899,8 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
8559
8899
  codexThreadId: attribution.codexThreadId ?? null,
8560
8900
  channelSlug: attribution.channelSlug ?? null,
8561
8901
  processName: attribution.processName ?? null,
8562
- processIconKey: attribution.processIconKey ?? null
8902
+ processIconKey: attribution.processIconKey ?? null,
8903
+ localEditor: attribution.localEditor === true
8563
8904
  });
8564
8905
  };
8565
8906
  const clearForwardPortAttribution = (port) => {
@@ -8573,7 +8914,8 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
8573
8914
  codexThreadId: attribution?.codexThreadId ?? null,
8574
8915
  channelSlug: attribution?.channelSlug ?? null,
8575
8916
  processName: attribution?.processName ?? null,
8576
- processIconKey: attribution?.processIconKey ?? null
8917
+ processIconKey: attribution?.processIconKey ?? null,
8918
+ localEditor: attribution?.localEditor === true
8577
8919
  };
8578
8920
  });
8579
8921
  const allowedCwds = { value: [...options.allowedCwds] };
@@ -8584,11 +8926,35 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
8584
8926
  value: { status: "disabled" }
8585
8927
  };
8586
8928
  const localEditorGeneration = { value: 0 };
8929
+ const localEditorForwardState = {
8930
+ watcher: undefined,
8931
+ approvedPorts: new Set,
8932
+ approvedTargets: new Map,
8933
+ dismissedTargets: new Map,
8934
+ pendingRequests: new Map,
8935
+ queuedCandidates: new Map
8936
+ };
8587
8937
  cleanup.actions.push(() => {
8588
8938
  if (localEditorState.value.status === "running") {
8589
8939
  localEditorState.value.process.kill("SIGINT");
8590
8940
  localEditorState.value.collaboration?.process.kill("SIGINT");
8591
8941
  }
8942
+ localEditorForwardState.watcher?.close();
8943
+ });
8944
+ const pendingEditorForwardPortRequests = () => Array.from(localEditorForwardState.pendingRequests.values()).sort((left, right) => left.port - right.port).map((request) => {
8945
+ const processIdentity = guessCanonicalProcessFromCommand(request.command);
8946
+ const label = processIdentity?.appName ?? portForwardPromptLabel(approvedTargetFromRequest(request));
8947
+ return {
8948
+ requestId: request.requestId,
8949
+ port: request.port,
8950
+ pid: request.pid,
8951
+ command: request.command,
8952
+ ...request.cwd === undefined ? {} : { cwd: request.cwd },
8953
+ summary: `Make ${label} on port ${request.port} accessible on Linzumi?`,
8954
+ reason: portForwardPromptReason(approvedTargetFromRequest(request)),
8955
+ ...processIdentity?.appName === undefined ? {} : { processName: processIdentity.appName },
8956
+ ...processIdentity?.iconKey === undefined ? {} : { processIconKey: processIdentity.iconKey }
8957
+ };
8592
8958
  });
8593
8959
  const capabilitiesPayload = () => ({
8594
8960
  codexAppServer: true,
@@ -8600,6 +8966,7 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
8600
8966
  portForwarding: liveForwardPorts.size > 0,
8601
8967
  allowedPorts: Array.from(liveForwardPorts).sort((left, right) => left - right),
8602
8968
  forwardedPortAttributions: buildForwardPortAttributionPayload(),
8969
+ pendingEditorForwardPortRequests: pendingEditorForwardPortRequests(),
8603
8970
  toolStatus: options.dependencyStatus === undefined ? null : dependencyStatusPayload(options.dependencyStatus),
8604
8971
  editorRuntime: options.dependencyStatus?.editorRuntime === undefined ? null : dependencyStatusPayload(options.dependencyStatus).editorRuntime,
8605
8972
  ...localEditorCapabilities(options.editorRuntime, allowedCwds.value, localEditorState.value)
@@ -8611,6 +8978,7 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
8611
8978
  const joinPayload = () => ({
8612
8979
  clientName: "kandan-local-codex-runner",
8613
8980
  clientId,
8981
+ runnerPid: process.pid,
8614
8982
  version: linzumiCliVersion,
8615
8983
  workspace: runnerWorkspaceSlug(options) ?? null,
8616
8984
  channel: options.channelSession?.channelSlug ?? null,
@@ -8648,12 +9016,204 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
8648
9016
  });
8649
9017
  });
8650
9018
  };
9019
+ const pushEditorPortForwardEvent = (event, payload) => {
9020
+ kandan.push(topic, event, payload).catch((error) => {
9021
+ log(`kandan.${event}_push_failed`, {
9022
+ message: error instanceof Error ? error.message : String(error)
9023
+ });
9024
+ });
9025
+ };
9026
+ const revokeEditorForwardPort = (port, reason) => {
9027
+ const target = localEditorForwardState.approvedTargets.get(port);
9028
+ localEditorForwardState.approvedPorts.delete(port);
9029
+ localEditorForwardState.approvedTargets.delete(port);
9030
+ liveForwardPorts.delete(port);
9031
+ clearForwardPortAttribution(port);
9032
+ pushEditorPortForwardEvent("forward_port_revoked", {
9033
+ instanceId,
9034
+ port,
9035
+ ...target === undefined ? {} : {
9036
+ pid: target.pid,
9037
+ command: target.command,
9038
+ ...target.cwd === undefined ? {} : { cwd: target.cwd }
9039
+ },
9040
+ reason,
9041
+ capabilities: revocationCapabilities(capabilitiesPayload(), port)
9042
+ });
9043
+ };
9044
+ const clearEditorPortForwarding = (reason) => {
9045
+ localEditorForwardState.watcher?.close();
9046
+ localEditorForwardState.watcher = undefined;
9047
+ localEditorForwardState.pendingRequests.clear();
9048
+ localEditorForwardState.queuedCandidates.clear();
9049
+ const revokedPorts = Array.from(localEditorForwardState.approvedPorts);
9050
+ for (const port of revokedPorts) {
9051
+ revokeEditorForwardPort(port, reason);
9052
+ }
9053
+ localEditorForwardState.dismissedTargets.clear();
9054
+ return revokedPorts;
9055
+ };
9056
+ const drainQueuedEditorPortForwardPrompt = () => {
9057
+ if (localEditorForwardState.pendingRequests.size > 0 || localEditorForwardState.queuedCandidates.size === 0) {
9058
+ return;
9059
+ }
9060
+ const next = Array.from(localEditorForwardState.queuedCandidates.values()).sort((left, right) => left.port - right.port)[0];
9061
+ if (next === undefined) {
9062
+ return;
9063
+ }
9064
+ localEditorForwardState.queuedCandidates.delete(next.port);
9065
+ publishEditorPortForwardPrompt(next);
9066
+ if (localEditorForwardState.pendingRequests.size === 0) {
9067
+ drainQueuedEditorPortForwardPrompt();
9068
+ }
9069
+ };
9070
+ const publishEditorPortForwardPrompt = (candidate) => {
9071
+ const review = reviewPortForwardCandidate({
9072
+ candidate,
9073
+ threadBound: true,
9074
+ suppressedPorts: new Set(suppressedForwardPorts()),
9075
+ approvedPorts: localEditorForwardState.approvedPorts,
9076
+ approvedTargets: localEditorForwardState.approvedTargets,
9077
+ dismissedTargets: localEditorForwardState.dismissedTargets,
9078
+ pendingRequests: Array.from(localEditorForwardState.pendingRequests.values())
9079
+ });
9080
+ switch (review.type) {
9081
+ case "skip":
9082
+ return;
9083
+ case "remember_approved_target":
9084
+ localEditorForwardState.approvedTargets.set(review.target.port, review.target);
9085
+ return;
9086
+ case "revoke_and_prompt":
9087
+ if (localEditorForwardState.pendingRequests.size > 0) {
9088
+ localEditorForwardState.queuedCandidates.set(candidate.port, candidate);
9089
+ return;
9090
+ }
9091
+ revokeEditorForwardPort(review.revoked.port, review.reason);
9092
+ break;
9093
+ case "prompt":
9094
+ if (localEditorForwardState.pendingRequests.size > 0) {
9095
+ localEditorForwardState.queuedCandidates.set(candidate.port, candidate);
9096
+ return;
9097
+ }
9098
+ break;
9099
+ }
9100
+ const requestId = `editor-port-forward-${randomUUID3()}`;
9101
+ const request = pendingRequestFromCandidate({
9102
+ requestId,
9103
+ sourceSeq: 0,
9104
+ candidate
9105
+ });
9106
+ localEditorForwardState.pendingRequests.set(request.port, request);
9107
+ const processIdentity = guessCanonicalProcessFromCommand(candidate.command);
9108
+ pushEditorPortForwardEvent("forward_port_requested", {
9109
+ instanceId,
9110
+ requestId,
9111
+ source: "local_editor",
9112
+ port: request.port,
9113
+ pid: request.pid,
9114
+ command: request.command,
9115
+ ...request.cwd === undefined ? {} : { cwd: request.cwd },
9116
+ ...processIdentity?.appName === undefined ? {} : { processName: processIdentity.appName },
9117
+ ...processIdentity?.iconKey === undefined ? {} : { processIconKey: processIdentity.iconKey },
9118
+ capabilities: capabilitiesPayload()
9119
+ });
9120
+ };
9121
+ const expireLostEditorPortForwardCandidate = (candidate) => {
9122
+ const queuedCandidate = localEditorForwardState.queuedCandidates.get(candidate.port);
9123
+ if (queuedCandidate !== undefined && sameForwardCandidate(queuedCandidate, candidate)) {
9124
+ localEditorForwardState.queuedCandidates.delete(candidate.port);
9125
+ return;
9126
+ }
9127
+ const pendingRequest = localEditorForwardState.pendingRequests.get(candidate.port);
9128
+ if (pendingRequest !== undefined && sameForwardCandidate(approvedTargetFromRequest(pendingRequest), candidate)) {
9129
+ localEditorForwardState.pendingRequests.delete(candidate.port);
9130
+ pushEditorPortForwardEvent("forward_port_revoked", {
9131
+ instanceId,
9132
+ port: candidate.port,
9133
+ pid: candidate.pid,
9134
+ command: candidate.command,
9135
+ ...candidate.cwd === undefined ? {} : { cwd: candidate.cwd },
9136
+ reason: "listener_exited",
9137
+ capabilities: revocationCapabilities(capabilitiesPayload(), candidate.port)
9138
+ });
9139
+ drainQueuedEditorPortForwardPrompt();
9140
+ return;
9141
+ }
9142
+ if (localEditorForwardState.approvedPorts.has(candidate.port)) {
9143
+ revokeEditorForwardPort(candidate.port, "listener_exited");
9144
+ }
9145
+ };
9146
+ const resolveEditorPortForwardRequest = (control) => {
9147
+ const request = Array.from(localEditorForwardState.pendingRequests.values()).find((request2) => request2.requestId === control.requestId);
9148
+ if (request === undefined) {
9149
+ return;
9150
+ }
9151
+ localEditorForwardState.pendingRequests.delete(request.port);
9152
+ if (control.decision === "deny") {
9153
+ localEditorForwardState.dismissedTargets.set(request.port, approvedTargetFromRequest(request));
9154
+ pushEditorPortForwardEvent("forward_port_resolved", {
9155
+ instanceId,
9156
+ requestId: request.requestId,
9157
+ source: "local_editor",
9158
+ port: request.port,
9159
+ pid: request.pid,
9160
+ command: request.command,
9161
+ ...request.cwd === undefined ? {} : { cwd: request.cwd },
9162
+ decision: "deny",
9163
+ capabilities: capabilitiesPayload()
9164
+ });
9165
+ drainQueuedEditorPortForwardPrompt();
9166
+ return { instanceId, ok: true };
9167
+ }
9168
+ localEditorForwardState.approvedPorts.add(request.port);
9169
+ localEditorForwardState.approvedTargets.set(request.port, approvedTargetFromRequest(request));
9170
+ liveForwardPorts.add(request.port);
9171
+ const processIdentity = guessCanonicalProcessFromCommand(request.command);
9172
+ setForwardPortAttribution(request.port, {
9173
+ localEditor: true,
9174
+ processName: processIdentity?.appName ?? null,
9175
+ processIconKey: processIdentity?.iconKey ?? null
9176
+ });
9177
+ pushEditorPortForwardEvent("forward_port_resolved", {
9178
+ instanceId,
9179
+ requestId: request.requestId,
9180
+ source: "local_editor",
9181
+ port: request.port,
9182
+ pid: request.pid,
9183
+ command: request.command,
9184
+ ...request.cwd === undefined ? {} : { cwd: request.cwd },
9185
+ decision: "approve",
9186
+ capabilities: capabilitiesPayload()
9187
+ });
9188
+ drainQueuedEditorPortForwardPrompt();
9189
+ return { instanceId, ok: true, port: request.port };
9190
+ };
9191
+ const startLocalEditorPortForwardWatcher = (state) => {
9192
+ if (state.process.pid === undefined) {
9193
+ log("port_forward.local_editor_watch_skipped", {
9194
+ reason: "editor_pid_missing"
9195
+ });
9196
+ return;
9197
+ }
9198
+ localEditorForwardState.watcher?.close();
9199
+ localEditorForwardState.watcher = startPortForwardWatcher({
9200
+ rootPid: state.process.pid,
9201
+ onCandidate: publishEditorPortForwardPrompt,
9202
+ onCandidateLost: expireLostEditorPortForwardCandidate,
9203
+ onError: (error) => {
9204
+ log("port_forward.local_editor_watch_failed", {
9205
+ message: error.message
9206
+ });
9207
+ }
9208
+ });
9209
+ };
8651
9210
  const watchLocalEditorExit = (state, generation, initialStatusPushed) => {
8652
9211
  const handleExit = () => {
8653
9212
  if (localEditorGeneration.value !== generation || localEditorState.value.status !== "running" || localEditorState.value.process !== state.process) {
8654
9213
  return;
8655
9214
  }
8656
9215
  localEditorState.value = { status: "disabled" };
9216
+ const revokedEditorPorts = clearEditorPortForwarding("editor_exited");
8657
9217
  liveForwardPorts.delete(state.port);
8658
9218
  managedForwardPorts.delete(state.port);
8659
9219
  if (state.collaboration !== undefined) {
@@ -8666,7 +9226,11 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
8666
9226
  cwd: state.cwd,
8667
9227
  capabilities: {
8668
9228
  ...capabilitiesPayload(),
8669
- revokedPorts: state.collaboration === undefined ? [state.port] : [state.port, state.collaboration.serverPort]
9229
+ revokedPorts: state.collaboration === undefined ? [state.port, ...revokedEditorPorts] : [
9230
+ state.port,
9231
+ state.collaboration.serverPort,
9232
+ ...revokedEditorPorts
9233
+ ]
8670
9234
  }
8671
9235
  });
8672
9236
  };
@@ -8724,7 +9288,7 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
8724
9288
  launchTui: options.launchTui,
8725
9289
  enablePortForwardWatch: true,
8726
9290
  initialForwardPorts: allowedForwardPorts,
8727
- suppressedForwardPorts: () => Array.from(managedForwardPorts),
9291
+ suppressedForwardPorts,
8728
9292
  onForwardPortApproved: (port, attribution) => {
8729
9293
  liveForwardPorts.add(port);
8730
9294
  setForwardPortAttribution(port, attribution);
@@ -8779,7 +9343,7 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
8779
9343
  launchTui: false,
8780
9344
  enablePortForwardWatch: true,
8781
9345
  initialForwardPorts: allowedForwardPorts,
8782
- suppressedForwardPorts: () => Array.from(managedForwardPorts),
9346
+ suppressedForwardPorts,
8783
9347
  onForwardPortApproved: (port, attribution) => {
8784
9348
  liveForwardPorts.add(port);
8785
9349
  setForwardPortAttribution(port, attribution);
@@ -8950,6 +9514,11 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
8950
9514
  if (result.ok) {
8951
9515
  localEditorGeneration.value += 1;
8952
9516
  const editorGeneration = localEditorGeneration.value;
9517
+ const restartedEditorRevokedPorts = localEditorState.value.status === "running" ? [
9518
+ localEditorState.value.port,
9519
+ ...localEditorState.value.collaboration === undefined ? [] : [localEditorState.value.collaboration.serverPort],
9520
+ ...clearEditorPortForwarding("editor_restarted")
9521
+ ] : [];
8953
9522
  if (localEditorState.value.status === "running") {
8954
9523
  liveForwardPorts.delete(localEditorState.value.port);
8955
9524
  managedForwardPorts.delete(localEditorState.value.port);
@@ -8969,10 +9538,14 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
8969
9538
  instanceId,
8970
9539
  requestId: control.requestId,
8971
9540
  ok: true,
8972
- capabilities: capabilitiesPayload(),
9541
+ capabilities: {
9542
+ ...capabilitiesPayload(),
9543
+ ...restartedEditorRevokedPorts.length === 0 ? {} : { revokedPorts: restartedEditorRevokedPorts }
9544
+ },
8973
9545
  ...result.event
8974
9546
  });
8975
9547
  if (result.state.status === "running") {
9548
+ startLocalEditorPortForwardWatcher(result.state);
8976
9549
  watchLocalEditorExit(result.state, editorGeneration, initialStatusPushed);
8977
9550
  }
8978
9551
  return initialStatusPushed;
@@ -9005,6 +9578,17 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
9005
9578
  pushHeartbeat();
9006
9579
  return;
9007
9580
  }
9581
+ if (isResolvePortForwardRequestControl(control)) {
9582
+ const response = resolveEditorPortForwardRequest(control);
9583
+ if (response !== undefined) {
9584
+ kandan.push(topic, "codex_response", response).catch((error) => {
9585
+ log("kandan.control_response_push_failed", {
9586
+ message: error instanceof Error ? error.message : String(error)
9587
+ });
9588
+ });
9589
+ return;
9590
+ }
9591
+ }
9008
9592
  if (isSetPortForwardEnabledControl(control)) {
9009
9593
  switch (control.enabled) {
9010
9594
  case true:
@@ -9014,6 +9598,8 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
9014
9598
  liveForwardPorts.delete(control.port);
9015
9599
  managedForwardPorts.delete(control.port);
9016
9600
  clearForwardPortAttribution(control.port);
9601
+ localEditorForwardState.approvedPorts.delete(control.port);
9602
+ localEditorForwardState.approvedTargets.delete(control.port);
9017
9603
  kandan.push(topic, "forward_port_revoked", {
9018
9604
  instanceId,
9019
9605
  port: control.port,
@@ -9205,6 +9791,24 @@ function codexTuiArgs(codexUrl, codexThreadId, session, fast) {
9205
9791
  const overrides = codexTuiConfigArgs(session, fast);
9206
9792
  return codexThreadId === undefined ? ["--remote", codexUrl, ...overrides] : ["resume", "--remote", codexUrl, ...overrides, codexThreadId];
9207
9793
  }
9794
+ function suppressedForwardPortsForRunner(kandanControlPort, managedForwardPorts) {
9795
+ const suppressedPorts = new Set(managedForwardPorts);
9796
+ if (kandanControlPort !== undefined) {
9797
+ suppressedPorts.add(kandanControlPort);
9798
+ }
9799
+ return Array.from(suppressedPorts).sort((left, right) => left - right);
9800
+ }
9801
+ function explicitUrlPort(url) {
9802
+ const parsed = new URL(url);
9803
+ if (parsed.port === "") {
9804
+ return;
9805
+ }
9806
+ const port = Number.parseInt(parsed.port, 10);
9807
+ if (!Number.isInteger(port) || port <= 0 || port > 65535) {
9808
+ throw new Error(`invalid Kandan URL port: ${url}`);
9809
+ }
9810
+ return port;
9811
+ }
9208
9812
  function codexTuiConfigArgs(session, fast) {
9209
9813
  const modelArgs = session?.model === undefined ? [] : ["--model", session.model];
9210
9814
  const reasoningArgs = session?.reasoningEffort === undefined ? [] : ["-c", `model_reasoning_effort="${session.reasoningEffort}"`];
@@ -9621,6 +10225,9 @@ function isUpdateRunnerConfigControl(control) {
9621
10225
  function isSetPortForwardEnabledControl(control) {
9622
10226
  return control.type === "set_port_forward_enabled" && Number.isInteger(control.port) && control.port > 0 && control.port <= 65535;
9623
10227
  }
10228
+ function isResolvePortForwardRequestControl(control) {
10229
+ return control.type === "resolve_port_forward_request" && typeof control.requestId === "string" && (control.decision === "approve" || control.decision === "deny");
10230
+ }
9624
10231
  function normalizeAllowedCwds(values) {
9625
10232
  return Array.from(new Set(values.flatMap((value) => {
9626
10233
  const normalized = value.trim();
@@ -9656,7 +10263,7 @@ function allowedCwdSuggestions(cwd, allowedCwds) {
9656
10263
  }
9657
10264
 
9658
10265
  // src/authCache.ts
9659
- import { existsSync as existsSync6, mkdirSync as mkdirSync7, readFileSync as readFileSync5, writeFileSync as writeFileSync5 } from "node:fs";
10266
+ import { existsSync as existsSync6, mkdirSync as mkdirSync7, readFileSync as readFileSync6, writeFileSync as writeFileSync5 } from "node:fs";
9660
10267
  import { homedir as homedir6 } from "node:os";
9661
10268
  import { dirname as dirname6, join as join9 } from "node:path";
9662
10269
  function defaultAuthFilePath() {
@@ -9667,7 +10274,7 @@ function readCachedLocalRunnerToken(kandanUrl, authFilePath = defaultAuthFilePat
9667
10274
  if (!existsSync6(authFilePath)) {
9668
10275
  return;
9669
10276
  }
9670
- const authFile = parseAuthFile(readFileSync5(authFilePath, "utf8"));
10277
+ const authFile = parseAuthFile(readFileSync6(authFilePath, "utf8"));
9671
10278
  const kandanBaseUrl = kandanHttpBaseUrl(kandanUrl);
9672
10279
  const entry = authFile.local_codex_runner?.[kandanBaseUrl];
9673
10280
  if (entry === undefined || entry.access_token.trim() === "") {
@@ -9685,7 +10292,7 @@ function readCachedLocalRunnerToken(kandanUrl, authFilePath = defaultAuthFilePat
9685
10292
  }
9686
10293
  function writeCachedLocalRunnerToken(args) {
9687
10294
  const authFilePath = args.authFilePath ?? defaultAuthFilePath();
9688
- const existing = existsSync6(authFilePath) ? parseAuthFile(readFileSync5(authFilePath, "utf8")) : { version: 1 };
10295
+ const existing = existsSync6(authFilePath) ? parseAuthFile(readFileSync6(authFilePath, "utf8")) : { version: 1 };
9689
10296
  const kandanBaseUrl = kandanHttpBaseUrl(args.kandanUrl);
9690
10297
  const issuedAt = new Date;
9691
10298
  const expiresAt = args.expiresInSeconds === undefined ? undefined : new Date(issuedAt.getTime() + args.expiresInSeconds * 1000).toISOString();
@@ -9794,12 +10401,8 @@ async function acquireAndCacheToken(args) {
9794
10401
  return token.accessToken;
9795
10402
  }
9796
10403
 
9797
- // src/defaultUrls.ts
9798
- var defaultLinzumiHttpUrl = "https://serve.linzumi.com";
9799
- var defaultLinzumiWebSocketUrl = "wss://serve.linzumi.com";
9800
-
9801
10404
  // src/kandanTls.ts
9802
- import { existsSync as existsSync7, readFileSync as readFileSync6 } from "node:fs";
10405
+ import { existsSync as existsSync7, readFileSync as readFileSync7 } from "node:fs";
9803
10406
  import { Agent } from "undici";
9804
10407
  import { WebSocket as WsWebSocket } from "ws";
9805
10408
  function kandanTlsTrustFromEnv() {
@@ -9813,7 +10416,7 @@ function kandanTlsTrustFromCaFile(caFile) {
9813
10416
  if (!existsSync7(trimmed)) {
9814
10417
  throw new Error(`KANDAN_TLS_CA_FILE does not exist: ${trimmed}`);
9815
10418
  }
9816
- const ca = readFileSync6(trimmed, "utf8");
10419
+ const ca = readFileSync7(trimmed, "utf8");
9817
10420
  return {
9818
10421
  caFile: trimmed,
9819
10422
  ca,
@@ -9842,7 +10445,7 @@ function trustedWebSocketFactory(trust, WebSocketImpl = WsWebSocket) {
9842
10445
  }
9843
10446
 
9844
10447
  // src/agentBootstrap.ts
9845
- import { existsSync as existsSync8, mkdirSync as mkdirSync8, readFileSync as readFileSync7, writeFileSync as writeFileSync6 } from "node:fs";
10448
+ import { existsSync as existsSync8, mkdirSync as mkdirSync8, readFileSync as readFileSync8, writeFileSync as writeFileSync6 } from "node:fs";
9846
10449
  import { dirname as dirname7, join as join10 } from "node:path";
9847
10450
  import { homedir as homedir7 } from "node:os";
9848
10451
  async function runAgentCliCommand(args, deps = {
@@ -10452,7 +11055,7 @@ function authorizationHeaders(token) {
10452
11055
  return { authorization: `Bearer ${token}` };
10453
11056
  }
10454
11057
  function readOptionalTextFile(path) {
10455
- return existsSync8(path) ? readFileSync7(path, "utf8") : undefined;
11058
+ return existsSync8(path) ? readFileSync8(path, "utf8") : undefined;
10456
11059
  }
10457
11060
  function writeTextFile(path, content) {
10458
11061
  mkdirSync8(dirname7(path), { recursive: true });
@@ -10532,7 +11135,7 @@ Launch target:
10532
11135
  }
10533
11136
 
10534
11137
  // src/helloLinzumiProject.ts
10535
- import { existsSync as existsSync9, mkdirSync as mkdirSync9, readFileSync as readFileSync8, rmSync as rmSync2, writeFileSync as writeFileSync7 } from "node:fs";
11138
+ import { existsSync as existsSync9, mkdirSync as mkdirSync9, readFileSync as readFileSync9, rmSync as rmSync2, writeFileSync as writeFileSync7 } from "node:fs";
10536
11139
  import { dirname as dirname8, join as join11, resolve as resolve7 } from "node:path";
10537
11140
  import { fileURLToPath } from "node:url";
10538
11141
  var defaultHelloLinzumiProjectDir = "/tmp/hello_linzumi";
@@ -10542,7 +11145,7 @@ var defaultHelloLinzumiPort = 8787;
10542
11145
  var defaultHelloLinzumiHost = "0.0.0.0";
10543
11146
  var markerFile = ".linzumi-demo-project";
10544
11147
  var moduleDir = dirname8(fileURLToPath(import.meta.url));
10545
- var linzumiLogoSvg = readFileSync8(join11(moduleDir, "assets", "linzumi-logo.svg"), "utf8");
11148
+ var linzumiLogoSvg = readFileSync9(join11(moduleDir, "assets", "linzumi-logo.svg"), "utf8");
10546
11149
  function createHelloLinzumiProject(input = {}) {
10547
11150
  const options = typeof input === "string" ? { rootPath: input } : input;
10548
11151
  const root = resolveHelloProjectRoot(options);
@@ -10597,7 +11200,7 @@ function assertWritableDemoRoot(root, reset) {
10597
11200
  return;
10598
11201
  }
10599
11202
  const markerPath = join11(root, markerFile);
10600
- const isDemoRoot = existsSync9(markerPath) && readFileSync8(markerPath, "utf8").trim() === "hello-linzumi";
11203
+ const isDemoRoot = existsSync9(markerPath) && readFileSync9(markerPath, "utf8").trim() === "hello-linzumi";
10601
11204
  if (isDemoRoot && reset) {
10602
11205
  rmSync2(root, { recursive: true, force: true });
10603
11206
  return;
@@ -11100,7 +11703,7 @@ import {
11100
11703
  closeSync as closeSync2,
11101
11704
  mkdirSync as mkdirSync10,
11102
11705
  openSync as openSync3,
11103
- readFileSync as readFileSync9,
11706
+ readFileSync as readFileSync10,
11104
11707
  watch,
11105
11708
  writeFileSync as writeFileSync8
11106
11709
  } from "node:fs";
@@ -11185,12 +11788,12 @@ function commanderDaemonStatus(runnerId, statusDir = commanderStatusDir(), proce
11185
11788
  if (!existsSync10(statusFile)) {
11186
11789
  return { status: "missing", runnerId, statusFile };
11187
11790
  }
11188
- const record = parseRecord(readFileSync9(statusFile, "utf8"));
11791
+ const record = parseRecord(readFileSync10(statusFile, "utf8"));
11189
11792
  return processIsRunning(record.pid) && processMatchesRecord(record, processIdentityReader) ? { status: "running", record } : { status: "stopped", record };
11190
11793
  }
11191
11794
  async function waitForCommanderDaemon(options) {
11192
11795
  const now = options.now ?? (() => Date.now());
11193
- const readTextFile = options.readTextFile ?? ((path) => existsSync10(path) ? readFileSync9(path, "utf8") : undefined);
11796
+ const readTextFile = options.readTextFile ?? ((path) => existsSync10(path) ? readFileSync10(path, "utf8") : undefined);
11194
11797
  const statusImpl = options.statusImpl ?? commanderDaemonStatus;
11195
11798
  const deadline = now() + options.timeoutMs;
11196
11799
  while (now() <= deadline) {
@@ -11445,7 +12048,7 @@ async function main(args) {
11445
12048
  }
11446
12049
  case "start": {
11447
12050
  const options = await parseStartRunnerArgs(parsed.args);
11448
- addAllowedCwd(options.cwd);
12051
+ addAllowedCwdForLinzumiUrl(options.cwd, options.kandanUrl);
11449
12052
  await runLocalCodexRunner(withLocalMachineId(options));
11450
12053
  return;
11451
12054
  }
@@ -11548,19 +12151,16 @@ function runHelloCommand(args) {
11548
12151
  `);
11549
12152
  }
11550
12153
  function runPathsCommand(args) {
11551
- const [subcommand, pathValue, ...rest] = args;
11552
- if (subcommand === undefined || subcommand === "help" || subcommand === "--help") {
12154
+ const { subcommand, pathValue, linzumiUrl, help } = parsePathsCommandArgs(args);
12155
+ if (help || subcommand === undefined || subcommand === "help") {
11553
12156
  process.stdout.write(pathsHelpText());
11554
12157
  return;
11555
12158
  }
11556
- if (rest.length > 0) {
11557
- throw new Error("linzumi paths accepts one path argument");
11558
- }
11559
12159
  switch (subcommand) {
11560
12160
  case "list": {
11561
- const config = readLocalConfig();
12161
+ const config = linzumiUrl === undefined ? readLocalConfig() : readLocalConfigForLinzumiUrl(linzumiUrl);
11562
12162
  if (config.allowedCwds.length === 0) {
11563
- process.stdout.write(`No trusted paths configured in ${localConfigPath()}
12163
+ process.stdout.write(`No trusted paths configured in ${localConfigPath()}${pathsScopeSuffix(linzumiUrl)}
11564
12164
  `);
11565
12165
  return;
11566
12166
  }
@@ -11574,7 +12174,11 @@ function runPathsCommand(args) {
11574
12174
  throw new Error("missing path for linzumi paths add");
11575
12175
  }
11576
12176
  const trustedPath = realpathSync6(resolve9(expandUserPath(pathValue)));
11577
- addAllowedCwd(pathValue);
12177
+ if (linzumiUrl === undefined) {
12178
+ addAllowedCwd(pathValue);
12179
+ } else {
12180
+ addAllowedCwdForLinzumiUrl(pathValue, linzumiUrl);
12181
+ }
11578
12182
  process.stdout.write(`Trusted ${trustedPath}
11579
12183
  `);
11580
12184
  return;
@@ -11583,7 +12187,11 @@ function runPathsCommand(args) {
11583
12187
  if (pathValue === undefined || pathValue.trim() === "") {
11584
12188
  throw new Error("missing path for linzumi paths remove");
11585
12189
  }
11586
- removeAllowedCwd(pathValue);
12190
+ if (linzumiUrl === undefined) {
12191
+ removeAllowedCwd(pathValue);
12192
+ } else {
12193
+ removeAllowedCwdForLinzumiUrl(pathValue, linzumiUrl);
12194
+ }
11587
12195
  process.stdout.write(`Removed trusted path ${pathValue}
11588
12196
  `);
11589
12197
  return;
@@ -11592,6 +12200,46 @@ function runPathsCommand(args) {
11592
12200
  throw new Error(`invalid paths command: ${subcommand}`);
11593
12201
  }
11594
12202
  }
12203
+ function parsePathsCommandArgs(args) {
12204
+ const positional = [];
12205
+ let linzumiUrl;
12206
+ let help = false;
12207
+ for (let index = 0;index < args.length; index += 1) {
12208
+ const arg = args[index];
12209
+ switch (arg) {
12210
+ case "--help":
12211
+ help = true;
12212
+ break;
12213
+ case "--linzumi-url":
12214
+ case "--kandan-url": {
12215
+ const value = args[index + 1];
12216
+ if (value === undefined || value.startsWith("--")) {
12217
+ throw new Error(`missing value for ${arg}`);
12218
+ }
12219
+ linzumiUrl = value;
12220
+ index += 1;
12221
+ break;
12222
+ }
12223
+ default:
12224
+ if (arg !== undefined && arg.startsWith("--")) {
12225
+ throw new Error(`invalid flag: ${arg}`);
12226
+ }
12227
+ positional.push(arg ?? "");
12228
+ }
12229
+ }
12230
+ if (positional.length > 2) {
12231
+ throw new Error("linzumi paths accepts one path argument");
12232
+ }
12233
+ return {
12234
+ subcommand: positional[0],
12235
+ pathValue: positional[1],
12236
+ linzumiUrl,
12237
+ help
12238
+ };
12239
+ }
12240
+ function pathsScopeSuffix(linzumiUrl) {
12241
+ return linzumiUrl === undefined ? "" : ` for ${linzumiUrl}`;
12242
+ }
11595
12243
  async function runCommanderDaemonCommand(args) {
11596
12244
  const [subcommand, ...rest] = args;
11597
12245
  switch (subcommand) {
@@ -11823,7 +12471,7 @@ async function parseAgentRunnerArgs(args, deps = {
11823
12471
  const kandanUrl = stringValue3(values, "linzumi-url") ?? agentApiUrlToKandanUrl(tokenFile.apiUrl);
11824
12472
  const requestedCwdValue = cwdArg ?? stringValue3(values, "cwd");
11825
12473
  const requestedCwd = resolveUserPath(requestedCwdValue ?? process.cwd());
11826
- const configuredAllowedCwds2 = requestedCwdValue === undefined && !values.has("allowed-cwd") ? readConfiguredAllowedCwdDetails() : { allowedCwds: [], missingAllowedCwds: [] };
12474
+ const configuredAllowedCwds2 = requestedCwdValue === undefined && !values.has("allowed-cwd") ? readConfiguredAllowedCwdDetailsForLinzumiUrl(kandanUrl) : { allowedCwds: [], missingAllowedCwds: [] };
11827
12475
  const allowedCwds = values.has("allowed-cwd") ? assertConfiguredAllowedCwds(parseAllowedCwdList(stringValue3(values, "allowed-cwd"))) : requestedCwdValue === undefined ? configuredAllowedCwds2.allowedCwds.length > 0 ? [...configuredAllowedCwds2.allowedCwds] : assertConfiguredAllowedCwds([requestedCwd]) : assertConfiguredAllowedCwds([requestedCwd]);
11828
12476
  const cwd = allowedCwds[0] ?? requestedCwd;
11829
12477
  const codexBin = stringValue3(values, "codex-bin") ?? "codex";
@@ -11870,7 +12518,7 @@ async function parseAgentRunnerArgs(args, deps = {
11870
12518
  };
11871
12519
  }
11872
12520
  function readAgentTokenTextFile(path) {
11873
- return existsSync11(path) ? readFileSync10(path, "utf8") : undefined;
12521
+ return existsSync11(path) ? readFileSync11(path, "utf8") : undefined;
11874
12522
  }
11875
12523
  function rejectAgentRunnerTargetingFlags(values) {
11876
12524
  const unsupportedFlags = [
@@ -11954,7 +12602,7 @@ async function parseRunnerArgs(args, deps = {
11954
12602
  const kandanUrl = stringValue3(values, "linzumi-url") ?? defaultLinzumiWebSocketUrl;
11955
12603
  const cwd = stringValue3(values, "cwd") ?? process.cwd();
11956
12604
  const cwdAllowedCwds = assertConfiguredAllowedCwds([cwd]);
11957
- const localConfiguredAllowedCwds = values.has("allowed-cwd") ? { allowedCwds: [], missingAllowedCwds: [] } : readConfiguredAllowedCwdDetails();
12605
+ const localConfiguredAllowedCwds = values.has("allowed-cwd") ? { allowedCwds: [], missingAllowedCwds: [] } : readConfiguredAllowedCwdDetailsForLinzumiUrl(kandanUrl);
11958
12606
  const configuredAllowedCwds2 = values.has("allowed-cwd") ? assertConfiguredAllowedCwds(parseAllowedCwdList(stringValue3(values, "allowed-cwd"))) : [...localConfiguredAllowedCwds.allowedCwds];
11959
12607
  const codexBin = stringValue3(values, "codex-bin") ?? "codex";
11960
12608
  const customCodeServerBin = stringValue3(values, "code-server-bin");
@@ -12167,7 +12815,7 @@ function parseChannelPath(channel) {
12167
12815
  function withLocalMachineId(options) {
12168
12816
  return {
12169
12817
  ...options,
12170
- machineId: ensureLocalMachineId()
12818
+ machineId: localConfigScopeKey(options.kandanUrl) === localConfigScopeKey(defaultLinzumiWebSocketUrl) ? ensureLocalMachineId() : ensureLocalMachineIdForLinzumiUrl(options.kandanUrl)
12171
12819
  };
12172
12820
  }
12173
12821
  function required(values, key) {
@@ -12349,13 +12997,13 @@ function pathsHelpText() {
12349
12997
  return `Linzumi trusted paths
12350
12998
 
12351
12999
  Usage:
12352
- linzumi paths list
12353
- linzumi paths add <path>
12354
- linzumi paths remove <path>
13000
+ linzumi paths [--linzumi-url <ws-url>] list
13001
+ linzumi paths [--linzumi-url <ws-url>] add <path>
13002
+ linzumi paths [--linzumi-url <ws-url>] remove <path>
12355
13003
 
12356
- Trusted paths are stored in ~/.linzumi/config.json. linzumi connect always
12357
- trusts its selected cwd for that runner process and also uses configured paths
12358
- unless --allowed-cwd is passed with explicit extra roots.
13004
+ Trusted paths are stored in ~/.linzumi/config.json. Production/default paths
13005
+ use the root config fields; explicit --linzumi-url paths use a URL-scoped config
13006
+ section so local, staging, and production runners do not share trust state.
12359
13007
  `;
12360
13008
  }
12361
13009
  function startHelpText() {