@linzumi/cli 0.0.41-beta → 0.0.43-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 +645 -191
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -63,7 +63,7 @@ Install the CLI or run it with `npx`:
63
63
 
64
64
  ```bash
65
65
  npm install -g @linzumi/cli@latest
66
- npx -y @linzumi/cli@0.0.41-beta --version
66
+ npx -y @linzumi/cli@0.0.43-beta --version
67
67
  linzumi --version
68
68
  ```
69
69
 
package/dist/index.js CHANGED
@@ -1,16 +1,16 @@
1
1
  // src/index.ts
2
- import { randomUUID as randomUUID3 } from "node:crypto";
3
- import { existsSync as existsSync10, readFileSync as readFileSync9, realpathSync as realpathSync6 } from "node:fs";
2
+ import { randomUUID as randomUUID4 } from "node:crypto";
3
+ import { existsSync as existsSync11, readFileSync as readFileSync10, 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";
7
7
 
8
8
  // src/runner.ts
9
9
  import { spawn as spawn6 } from "node:child_process";
10
- import { randomUUID as randomUUID2 } from "node:crypto";
11
- import { realpathSync as realpathSync4 } from "node:fs";
10
+ import { randomUUID as randomUUID3 } from "node:crypto";
11
+ import { realpathSync as realpathSync5 } from "node:fs";
12
12
  import { hostname as hostname2 } from "node:os";
13
- import { join as join6, resolve as resolve5 } from "node:path";
13
+ import { join as join8, resolve as resolve6 } from "node:path";
14
14
 
15
15
  // src/channelSessionSupport.ts
16
16
  import { spawnSync } from "node:child_process";
@@ -248,7 +248,7 @@ function codexThreadRuntimeOverrides(options) {
248
248
  ...session.model === undefined ? {} : { model: session.model },
249
249
  ...session.reasoningEffort === undefined ? {} : { reasoningEffort: session.reasoningEffort },
250
250
  ...options.fast === true ? { serviceTier: "fast" } : {},
251
- ...session.approvalPolicy === undefined ? {} : { approvalPolicy: session.approvalPolicy },
251
+ approvalPolicy: codexApprovalPolicyForRequest(session.approvalPolicy, session.sandbox),
252
252
  ...session.sandbox === undefined ? {} : { sandbox: session.sandbox }
253
253
  };
254
254
  }
@@ -259,10 +259,30 @@ function codexTurnRuntimeOverrides(options) {
259
259
  ...session.model === undefined ? {} : { model: session.model },
260
260
  ...session.reasoningEffort === undefined ? {} : { effort: session.reasoningEffort },
261
261
  ...options.fast === true ? { serviceTier: "fast" } : {},
262
- ...session.approvalPolicy === undefined ? {} : { approvalPolicy: session.approvalPolicy },
262
+ approvalPolicy: codexApprovalPolicyForRequest(session.approvalPolicy, session.sandbox),
263
263
  ...session.sandbox === undefined ? {} : { sandboxPolicy: codexSandboxPolicy(session.sandbox, options.cwd) }
264
264
  };
265
265
  }
266
+ function codexApprovalPolicySetting(approvalPolicy, sandbox) {
267
+ if (approvalPolicy === undefined || approvalPolicy === "default") {
268
+ return;
269
+ }
270
+ return codexApprovalPolicyForRequest(approvalPolicy, sandbox);
271
+ }
272
+ function codexApprovalPolicyForRequest(approvalPolicy, sandbox) {
273
+ switch (approvalPolicy) {
274
+ case undefined:
275
+ case "default":
276
+ return "on-request";
277
+ case "never":
278
+ return sandbox === "danger-full-access" ? "never" : "on-request";
279
+ case "on-request":
280
+ case "on-failure":
281
+ return approvalPolicy;
282
+ default:
283
+ throw new Error(`unsupported Codex approval policy: ${approvalPolicy}`);
284
+ }
285
+ }
266
286
  function codexSandboxPolicy(sandbox, cwd) {
267
287
  switch (sandbox) {
268
288
  case "danger-full-access":
@@ -2441,6 +2461,8 @@ function shellCommandTokens(command) {
2441
2461
  var codexTypingHeartbeatMs = 5000;
2442
2462
  var defaultStreamFlushIntervalMs = 150;
2443
2463
  var maxForwardedTurnIds = 64;
2464
+ var maxClaimedKandanMessageKeys = 1024;
2465
+ var claimedKandanMessageKeys = new Set;
2444
2466
  async function attachChannelSession(args) {
2445
2467
  const session = args.options.channelSession;
2446
2468
  const chatTopic = `chat:${session.workspaceSlug}:${session.channelSlug}`;
@@ -2589,6 +2611,7 @@ async function attachChannelSession(args) {
2589
2611
  clearPendingStreamFlushTimers(state);
2590
2612
  rejectPendingApprovalRequests(state, new Error("runner closed"));
2591
2613
  await stopCodexTyping(args, state);
2614
+ releaseKandanMessageClaims(state);
2592
2615
  }
2593
2616
  };
2594
2617
  }
@@ -2630,6 +2653,7 @@ function initialChannelSessionState(cursor, rootSeq, kandanThreadId, codexThread
2630
2653
  forwardedTurnIds: new Set,
2631
2654
  forwardingTurnIds: new Set,
2632
2655
  retryableTurnIds: new Set,
2656
+ claimedKandanMessageKeys: new Set,
2633
2657
  localTuiTurnIds: new Set,
2634
2658
  mirroredTuiInputProjections: createBoundedCache(maxForwardedTurnIds),
2635
2659
  pendingTuiInputMirrors: new Map,
@@ -3356,6 +3380,15 @@ async function handleKandanChatEvent(args, state, runnerIdentity, payloadContext
3356
3380
  });
3357
3381
  return;
3358
3382
  }
3383
+ if (!claimKandanMessage(args, state, event)) {
3384
+ args.log("kandan.message_ignored", {
3385
+ seq: event.seq,
3386
+ actor_slug: event.actorSlug ?? null,
3387
+ actor_user_id: event.actorUserId ?? null,
3388
+ reason: "duplicate_message_claim"
3389
+ });
3390
+ return;
3391
+ }
3359
3392
  enqueuePendingKandanMessage(state.queue, {
3360
3393
  seq: event.seq,
3361
3394
  actorSlug: event.actorSlug,
@@ -3372,6 +3405,43 @@ async function handleKandanChatEvent(args, state, runnerIdentity, payloadContext
3372
3405
  await publishKandanMessageState(args, event, { status: "queued" });
3373
3406
  await drainKandanMessageQueue(args, state, payloadContext);
3374
3407
  }
3408
+ function kandanMessageClaimKey(args, threadId, seq) {
3409
+ const session = args.options.channelSession;
3410
+ return [
3411
+ args.options.runnerId,
3412
+ args.instanceId,
3413
+ session.workspaceSlug,
3414
+ session.channelSlug,
3415
+ threadId,
3416
+ String(seq)
3417
+ ].join(":");
3418
+ }
3419
+ function claimKandanMessage(args, state, event) {
3420
+ const threadId = event.threadId;
3421
+ if (threadId === undefined) {
3422
+ return false;
3423
+ }
3424
+ const key = kandanMessageClaimKey(args, threadId, event.seq);
3425
+ if (claimedKandanMessageKeys.has(key)) {
3426
+ return false;
3427
+ }
3428
+ claimedKandanMessageKeys.add(key);
3429
+ state.claimedKandanMessageKeys.add(key);
3430
+ if (state.claimedKandanMessageKeys.size > maxClaimedKandanMessageKeys) {
3431
+ const [expiredKey] = state.claimedKandanMessageKeys;
3432
+ if (expiredKey !== undefined) {
3433
+ state.claimedKandanMessageKeys.delete(expiredKey);
3434
+ claimedKandanMessageKeys.delete(expiredKey);
3435
+ }
3436
+ }
3437
+ return true;
3438
+ }
3439
+ function releaseKandanMessageClaims(state) {
3440
+ for (const key of state.claimedKandanMessageKeys) {
3441
+ claimedKandanMessageKeys.delete(key);
3442
+ }
3443
+ state.claimedKandanMessageKeys.clear();
3444
+ }
3375
3445
  async function startThreadMessageTurn(args, state, payloadContext, message) {
3376
3446
  if (state.kandanThreadId === undefined || state.codexThreadId === undefined) {
3377
3447
  throw new Error("cannot start a local Codex turn before thread binding");
@@ -3611,7 +3681,7 @@ async function handleCodexServerRequest(args, state, payloadContext, request) {
3611
3681
  throw new Error(message);
3612
3682
  }
3613
3683
  function codexApprovalRequestCanAutoAccept(settings, method) {
3614
- return settings.approvalPolicy === "never" && (method === "item/commandExecution/requestApproval" || method === "item/fileChange/requestApproval");
3684
+ return settings.approvalPolicy === "never" && settings.sandbox === "danger-full-access" && (method === "item/commandExecution/requestApproval" || method === "item/fileChange/requestApproval");
3615
3685
  }
3616
3686
  function codexApprovalRequestCanSurface(method) {
3617
3687
  return method === "item/commandExecution/requestApproval" || method === "item/fileChange/requestApproval";
@@ -5132,20 +5202,27 @@ function runtimeSettingsFromOptions(options) {
5132
5202
  return {
5133
5203
  model: options.channelSession.model,
5134
5204
  reasoningEffort: options.channelSession.reasoningEffort,
5135
- approvalPolicy: options.channelSession.approvalPolicy,
5205
+ approvalPolicy: codexApprovalPolicySetting(options.channelSession.approvalPolicy, options.channelSession.sandbox),
5136
5206
  sandbox: options.channelSession.sandbox,
5137
5207
  fast: options.fast
5138
5208
  };
5139
5209
  }
5140
5210
  function mergeRuntimeSettings(current, update) {
5211
+ const sandbox = mergeOptionalStringRuntimeSetting(current.sandbox, update, "sandbox");
5141
5212
  return {
5142
5213
  model: mergeOptionalStringRuntimeSetting(current.model, update, "model"),
5143
5214
  reasoningEffort: mergeOptionalStringRuntimeSetting(current.reasoningEffort, update, "reasoningEffort"),
5144
- approvalPolicy: mergeOptionalStringRuntimeSetting(current.approvalPolicy, update, "approvalPolicy"),
5145
- sandbox: mergeOptionalStringRuntimeSetting(current.sandbox, update, "sandbox"),
5215
+ approvalPolicy: mergeOptionalApprovalPolicyRuntimeSetting(current.approvalPolicy, update, sandbox),
5216
+ sandbox,
5146
5217
  fast: update.fast ?? current.fast
5147
5218
  };
5148
5219
  }
5220
+ function mergeOptionalApprovalPolicyRuntimeSetting(current, update, sandbox) {
5221
+ if (Object.prototype.hasOwnProperty.call(update, "approvalPolicy")) {
5222
+ return codexApprovalPolicySetting(update.approvalPolicy ?? undefined, sandbox);
5223
+ }
5224
+ return codexApprovalPolicySetting(current, sandbox);
5225
+ }
5149
5226
  function mergeOptionalStringRuntimeSetting(current, update, key) {
5150
5227
  if (Object.prototype.hasOwnProperty.call(update, key)) {
5151
5228
  return update[key] ?? undefined;
@@ -7947,6 +8024,356 @@ function waitForOpen2(websocket) {
7947
8024
  });
7948
8025
  }
7949
8026
 
8027
+ // src/runnerLock.ts
8028
+ import {
8029
+ closeSync,
8030
+ existsSync as existsSync5,
8031
+ mkdirSync as mkdirSync6,
8032
+ openSync as openSync2,
8033
+ readFileSync as readFileSync4,
8034
+ unlinkSync as unlinkSync2,
8035
+ writeSync
8036
+ } from "node:fs";
8037
+ import { dirname as dirname5, join as join7 } from "node:path";
8038
+
8039
+ // src/localConfig.ts
8040
+ import { randomUUID as randomUUID2 } from "node:crypto";
8041
+ import {
8042
+ existsSync as existsSync4,
8043
+ linkSync,
8044
+ mkdirSync as mkdirSync5,
8045
+ readFileSync as readFileSync3,
8046
+ realpathSync as realpathSync4,
8047
+ unlinkSync,
8048
+ writeFileSync as writeFileSync4
8049
+ } from "node:fs";
8050
+ import { homedir as homedir5 } from "node:os";
8051
+ import { basename as basename4, dirname as dirname4, join as join6, resolve as resolve5 } from "node:path";
8052
+ function localConfigPath(env = process.env) {
8053
+ const override = env.LINZUMI_CONFIG_FILE;
8054
+ return override !== undefined && override.trim() !== "" ? resolve5(expandUserPath(override)) : resolve5(homedir5(), ".linzumi", "config.json");
8055
+ }
8056
+ 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 };
8066
+ }
8067
+ function ensureLocalMachineId(path = localConfigPath(), createMachineId = randomUUID2) {
8068
+ const config = readLocalConfig(path);
8069
+ if (config.machineId !== undefined) {
8070
+ return config.machineId;
8071
+ }
8072
+ const machineId = ensureLocalMachineIdSeed(path, createMachineId);
8073
+ const latestConfig = readLocalConfig(path);
8074
+ const latestMachineId = latestConfig.machineId;
8075
+ if (latestMachineId !== undefined) {
8076
+ return latestMachineId;
8077
+ }
8078
+ writeLocalConfig({ ...latestConfig, machineId }, path);
8079
+ return machineId;
8080
+ }
8081
+ function localMachineIdSeedPath(configPath = localConfigPath()) {
8082
+ return join6(dirname4(configPath), `${basename4(configPath)}.machine-id`);
8083
+ }
8084
+ function readConfiguredAllowedCwdDetails(path = localConfigPath()) {
8085
+ const allowedCwds = [];
8086
+ const missingAllowedCwds = [];
8087
+ for (const cwd of readLocalConfig(path).allowedCwds) {
8088
+ const absolutePath = resolve5(expandUserPath(cwd));
8089
+ try {
8090
+ const realPath = realpathSync4(absolutePath);
8091
+ allowedCwds.push(...realPath === absolutePath ? [realPath] : [realPath, absolutePath]);
8092
+ } catch (error) {
8093
+ if (isMissingPathError(error)) {
8094
+ missingAllowedCwds.push(absolutePath);
8095
+ continue;
8096
+ }
8097
+ throw error;
8098
+ }
8099
+ }
8100
+ return {
8101
+ allowedCwds: uniqueStrings(allowedCwds),
8102
+ missingAllowedCwds: uniqueStrings(missingAllowedCwds)
8103
+ };
8104
+ }
8105
+ function addAllowedCwd(pathValue, path = localConfigPath()) {
8106
+ const normalizedPath = realpathSync4(resolve5(expandUserPath(pathValue)));
8107
+ const config = readLocalConfig(path);
8108
+ const allowedCwds = uniqueStrings([...config.allowedCwds, normalizedPath]);
8109
+ writeLocalConfig({ ...config, version: 1, allowedCwds }, path);
8110
+ return allowedCwds;
8111
+ }
8112
+ function removeAllowedCwd(pathValue, path = localConfigPath()) {
8113
+ const requestedPath = resolve5(expandUserPath(pathValue));
8114
+ const normalizedRequest = realpathOrResolved(requestedPath);
8115
+ const config = readLocalConfig(path);
8116
+ const allowedCwds = config.allowedCwds.filter((cwd) => {
8117
+ const normalizedExisting = realpathOrResolved(cwd);
8118
+ return cwd !== pathValue && normalizedExisting !== normalizedRequest;
8119
+ });
8120
+ writeLocalConfig({ ...config, version: 1, allowedCwds }, path);
8121
+ return allowedCwds;
8122
+ }
8123
+ function writeLocalConfig(config, path = localConfigPath()) {
8124
+ mkdirSync5(dirname4(path), { recursive: true });
8125
+ writeFileSync4(path, `${JSON.stringify(config, null, 2)}
8126
+ `, "utf8");
8127
+ }
8128
+ 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() !== "");
8130
+ }
8131
+ function machineIdValid(value) {
8132
+ 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
+ }
8134
+ function ensureLocalMachineIdSeed(configPath, createMachineId) {
8135
+ const seedPath = localMachineIdSeedPath(configPath);
8136
+ if (existsSync4(seedPath)) {
8137
+ return readMachineIdSeed(seedPath);
8138
+ }
8139
+ const machineId = createMachineId();
8140
+ if (!machineIdValid(machineId)) {
8141
+ throw new Error(`invalid generated Linzumi machine id: ${machineId}`);
8142
+ }
8143
+ mkdirSync5(dirname4(seedPath), { recursive: true });
8144
+ const tempPath = join6(dirname4(seedPath), `.${basename4(seedPath)}.${process.pid}.${randomUUID2()}.tmp`);
8145
+ writeFileSync4(tempPath, `${machineId}
8146
+ `, { encoding: "utf8", flag: "wx" });
8147
+ try {
8148
+ linkSync(tempPath, seedPath);
8149
+ return machineId;
8150
+ } catch (error) {
8151
+ if (isNodeErrorCode(error, "EEXIST")) {
8152
+ return readMachineIdSeed(seedPath);
8153
+ }
8154
+ throw error;
8155
+ } finally {
8156
+ unlinkSync(tempPath);
8157
+ }
8158
+ }
8159
+ function readMachineIdSeed(seedPath) {
8160
+ const machineId = readFileSync3(seedPath, "utf8").trim();
8161
+ if (!machineIdValid(machineId)) {
8162
+ throw new Error(`invalid Linzumi machine id seed: ${seedPath}`);
8163
+ }
8164
+ return machineId;
8165
+ }
8166
+ function uniqueStrings(values) {
8167
+ return [
8168
+ ...new Set(values.map((value) => value.trim()).filter((value) => value !== ""))
8169
+ ];
8170
+ }
8171
+ function isMissingPathError(error) {
8172
+ return typeof error === "object" && error !== null && "code" in error && (error.code === "ENOENT" || error.code === "ENOTDIR" || error.code === "EACCES" || error.code === "ELOOP" || error.code === "EIO");
8173
+ }
8174
+ function isNodeErrorCode(error, code) {
8175
+ return typeof error === "object" && error !== null && "code" in error && error.code === code;
8176
+ }
8177
+ function realpathOrResolved(pathValue) {
8178
+ try {
8179
+ return realpathSync4(resolve5(expandUserPath(pathValue)));
8180
+ } catch (_error) {
8181
+ return resolve5(expandUserPath(pathValue));
8182
+ }
8183
+ }
8184
+
8185
+ // src/version.ts
8186
+ var linzumiCliVersion = "0.0.43-beta";
8187
+ var linzumiCliVersionText = `linzumi ${linzumiCliVersion}`;
8188
+
8189
+ // src/runnerLock.ts
8190
+ function runnerLockPath(machineId, configPath = localConfigPath()) {
8191
+ return join7(dirname5(configPath), "runners", `${encodeURIComponent(machineId)}.lock`);
8192
+ }
8193
+ function acquireRunnerLock(options) {
8194
+ const path = runnerLockPath(options.machineId, options.configPath);
8195
+ const isPidAlive = options.isPidAlive ?? processIsAlive;
8196
+ const record = {
8197
+ version: 1,
8198
+ machineId: options.machineId,
8199
+ runnerId: options.runnerId,
8200
+ pid: options.pid ?? process.pid,
8201
+ cwd: options.cwd,
8202
+ workspace: options.workspace,
8203
+ startedAt: (options.now ?? (() => new Date))().toISOString(),
8204
+ cliVersion: options.cliVersion ?? linzumiCliVersion
8205
+ };
8206
+ writeLockOrHandleExisting(path, record, isPidAlive, options.beforeReadExistingLock, options.beforeReplaceStaleLock);
8207
+ return {
8208
+ path,
8209
+ record,
8210
+ release: () => releaseRunnerLock(path, record)
8211
+ };
8212
+ }
8213
+ function writeLockOrHandleExisting(path, record, isPidAlive, beforeReadExistingLock, beforeReplaceStaleLock) {
8214
+ if (tryCreateLock(path, record)) {
8215
+ return;
8216
+ }
8217
+ beforeReadExistingLock?.();
8218
+ const existing = readRunnerLockIfPresent(path);
8219
+ if (existing === undefined) {
8220
+ if (tryCreateLock(path, record)) {
8221
+ return;
8222
+ }
8223
+ throw new Error(`another Linzumi runner lock appeared while starting: ${path}`);
8224
+ }
8225
+ if (isPidAlive(existing.pid)) {
8226
+ throw new Error(activeRunnerLockMessage(path, existing));
8227
+ }
8228
+ beforeReplaceStaleLock?.();
8229
+ withStaleReplacementLock(path, isPidAlive, () => {
8230
+ const latest = existsSync5(path) ? readRunnerLock(path) : undefined;
8231
+ if (latest !== undefined && isPidAlive(latest.pid)) {
8232
+ throw new Error(activeRunnerLockMessage(path, latest));
8233
+ }
8234
+ if (latest !== undefined) {
8235
+ unlinkSync2(path);
8236
+ }
8237
+ if (!tryCreateLock(path, record)) {
8238
+ throw new Error(`another Linzumi runner lock appeared while starting: ${path}`);
8239
+ }
8240
+ });
8241
+ }
8242
+ function tryCreateLock(path, record) {
8243
+ mkdirSync6(dirname5(path), { recursive: true });
8244
+ try {
8245
+ const fd = openSync2(path, "wx");
8246
+ try {
8247
+ writeSync(fd, `${JSON.stringify(record, null, 2)}
8248
+ `);
8249
+ } finally {
8250
+ closeSync(fd);
8251
+ }
8252
+ return true;
8253
+ } catch (error) {
8254
+ if (isNodeErrorCode2(error, "EEXIST")) {
8255
+ return false;
8256
+ }
8257
+ throw error;
8258
+ }
8259
+ }
8260
+ function withStaleReplacementLock(path, isPidAlive, callback) {
8261
+ const replacementPath = `${path}.replace`;
8262
+ while (true) {
8263
+ try {
8264
+ const fd = openSync2(replacementPath, "wx");
8265
+ try {
8266
+ writeSync(fd, `${process.pid}
8267
+ `);
8268
+ callback();
8269
+ } finally {
8270
+ closeSync(fd);
8271
+ unlinkSync2(replacementPath);
8272
+ }
8273
+ return;
8274
+ } catch (error) {
8275
+ if (isNodeErrorCode2(error, "EEXIST")) {
8276
+ const replacementPid = readReplacementLockPidIfPresent(replacementPath);
8277
+ if (replacementPid === undefined) {
8278
+ continue;
8279
+ }
8280
+ if (isPidAlive(replacementPid)) {
8281
+ throw new Error([
8282
+ "another Linzumi runner is already replacing a stale runner lock",
8283
+ `lock: ${path}`,
8284
+ "Wait for that startup to finish, then retry."
8285
+ ].join(`
8286
+ `));
8287
+ }
8288
+ unlinkSync2(replacementPath);
8289
+ continue;
8290
+ }
8291
+ throw error;
8292
+ }
8293
+ }
8294
+ }
8295
+ function readReplacementLockPidIfPresent(path) {
8296
+ let value;
8297
+ try {
8298
+ value = readFileSync4(path, "utf8").trim();
8299
+ } catch (error) {
8300
+ if (isNodeErrorCode2(error, "ENOENT")) {
8301
+ return;
8302
+ }
8303
+ throw error;
8304
+ }
8305
+ const pid = Number.parseInt(value, 10);
8306
+ if (pid.toString() !== value || pid <= 0) {
8307
+ throw new Error(`invalid Linzumi runner replacement lock: ${path}`);
8308
+ }
8309
+ return pid;
8310
+ }
8311
+ function releaseRunnerLock(path, record) {
8312
+ const current = readRunnerLockForRelease(path);
8313
+ if (current !== undefined && current.machineId === record.machineId && current.runnerId === record.runnerId && current.pid === record.pid) {
8314
+ unlinkSync2(path);
8315
+ }
8316
+ }
8317
+ function readRunnerLockForRelease(path) {
8318
+ try {
8319
+ return readRunnerLockIfPresent(path);
8320
+ } catch (_error) {
8321
+ return;
8322
+ }
8323
+ }
8324
+ function readRunnerLockIfPresent(path) {
8325
+ if (!existsSync5(path)) {
8326
+ return;
8327
+ }
8328
+ try {
8329
+ return readRunnerLock(path);
8330
+ } catch (error) {
8331
+ if (isNodeErrorCode2(error, "ENOENT")) {
8332
+ return;
8333
+ }
8334
+ throw error;
8335
+ }
8336
+ }
8337
+ function readRunnerLock(path) {
8338
+ const parsed = JSON.parse(readFileSync4(path, "utf8"));
8339
+ if (!isRunnerLockRecord(parsed)) {
8340
+ throw new Error(`invalid Linzumi runner lock: ${path}`);
8341
+ }
8342
+ return parsed;
8343
+ }
8344
+ 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() !== "";
8346
+ }
8347
+ function workspaceValid(value) {
8348
+ return value === null || typeof value === "string" && value.trim() !== "";
8349
+ }
8350
+ function processIsAlive(pid) {
8351
+ try {
8352
+ process.kill(pid, 0);
8353
+ return true;
8354
+ } catch (error) {
8355
+ return isNodeErrorCode2(error, "ESRCH") ? false : true;
8356
+ }
8357
+ }
8358
+ function activeRunnerLockMessage(path, record) {
8359
+ const workspace = record.workspace === null ? "workspace: unknown" : `workspace: ${record.workspace}`;
8360
+ return [
8361
+ "another Linzumi runner is already running for this machine",
8362
+ `runner id: ${record.runnerId}`,
8363
+ `pid: ${record.pid}`,
8364
+ `cwd: ${record.cwd}`,
8365
+ workspace,
8366
+ `CLI version: ${record.cliVersion}`,
8367
+ `started at: ${record.startedAt}`,
8368
+ `lock: ${path}`,
8369
+ "Stop that process first, then retry. If the process has already exited, remove the stale lock file and retry."
8370
+ ].join(`
8371
+ `);
8372
+ }
8373
+ function isNodeErrorCode2(error, code) {
8374
+ return typeof error === "object" && error !== null && "code" in error && error.code === code;
8375
+ }
8376
+
7950
8377
  // src/runnerConsoleReporter.ts
7951
8378
  function reportRunnerConsoleEvent(event, payload) {
7952
8379
  const line = formatRunnerConsoleEvent(event, payload);
@@ -7958,7 +8385,15 @@ function reportRunnerConsoleEvent(event, payload) {
7958
8385
  function formatRunnerConsoleEvent(event, payload) {
7959
8386
  switch (event) {
7960
8387
  case "runner.instance_started":
7961
- return `Runner connected: instance=${text(payload.instanceId)} codex=${text(payload.codexUrl)}`;
8388
+ return connectedRunnerMessage(payload);
8389
+ case "runner.replaced":
8390
+ return [
8391
+ "Runner replaced: another Linzumi CLI connected from this machine.",
8392
+ `New runner: ${text(payload.replacementRunnerId)}`,
8393
+ `Version: ${text(payload.replacementVersion)}`,
8394
+ "This process is exiting."
8395
+ ].join(`
8396
+ `);
7962
8397
  case "kandan.message_ignored":
7963
8398
  return `Incoming message from ${sender(payload)}: ignored for reason ${text(payload.reason)}`;
7964
8399
  case "kandan.message_queued":
@@ -7993,6 +8428,40 @@ function formatRunnerConsoleEvent(event, payload) {
7993
8428
  return;
7994
8429
  }
7995
8430
  }
8431
+ function connectedRunnerMessage(payload) {
8432
+ return [
8433
+ "Connected to Linzumi",
8434
+ optionalLine("Computer", payload.hostname),
8435
+ optionalLine("Workspace", payload.workspace),
8436
+ `Runner: ${text(payload.runnerId)}`,
8437
+ `CLI: ${text(payload.version)}`,
8438
+ optionalLine("Codex", payload.codexUrl),
8439
+ ...replacementLines(payload.replacedRunners)
8440
+ ].filter((line) => line !== undefined).join(`
8441
+ `);
8442
+ }
8443
+ function optionalLine(label, value) {
8444
+ const normalized = stringValue2(value) ?? numberValue(value)?.toString();
8445
+ return normalized === undefined ? undefined : `${label}: ${normalized}`;
8446
+ }
8447
+ function replacementLines(value) {
8448
+ if (!Array.isArray(value)) {
8449
+ return [];
8450
+ }
8451
+ return value.flatMap((entry) => {
8452
+ if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
8453
+ return [];
8454
+ }
8455
+ const record = entry;
8456
+ const runnerId = stringValue2(record.runnerId);
8457
+ if (runnerId === undefined) {
8458
+ return [];
8459
+ }
8460
+ const version = stringValue2(record.version);
8461
+ const suffix = version === undefined ? "" : ` (CLI ${version})`;
8462
+ return [`Replaced older runner from this machine: ${runnerId}${suffix}`];
8463
+ });
8464
+ }
7996
8465
  function sender(payload) {
7997
8466
  const slug = stringValue2(payload.actor_slug);
7998
8467
  const userId = numberValue(payload.actor_user_id);
@@ -8056,6 +8525,21 @@ async function runLocalCodexRunner(options) {
8056
8525
  kandanUrl: options.kandanUrl
8057
8526
  });
8058
8527
  try {
8528
+ if (options.machineId !== undefined) {
8529
+ const runnerLock = acquireRunnerLock({
8530
+ machineId: options.machineId,
8531
+ runnerId: options.runnerId,
8532
+ cwd: options.cwd,
8533
+ workspace: runnerWorkspaceSlug(options) ?? null,
8534
+ configPath: options.runnerLockConfigPath
8535
+ });
8536
+ cleanup.actions.push(() => runnerLock.release());
8537
+ log("runner.lock_acquired", {
8538
+ path: runnerLock.path,
8539
+ machineId: options.machineId,
8540
+ runnerId: options.runnerId
8541
+ });
8542
+ }
8059
8543
  return await openLocalCodexRunner(options, log, cleanup, close);
8060
8544
  } catch (error) {
8061
8545
  await close().catch(() => {
@@ -8123,9 +8607,11 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
8123
8607
  const kandan = await connectPhoenixClient(options.kandanUrl, options.token, options.socketFactory);
8124
8608
  cleanup.actions.push(() => kandan.close());
8125
8609
  const topic = `local_runner:${options.runnerId}`;
8610
+ const clientId = options.machineId ?? options.runnerId;
8126
8611
  const joinPayload = () => ({
8127
8612
  clientName: "kandan-local-codex-runner",
8128
- version: "0.0.1",
8613
+ clientId,
8614
+ version: linzumiCliVersion,
8129
8615
  workspace: runnerWorkspaceSlug(options) ?? null,
8130
8616
  channel: options.channelSession?.channelSlug ?? null,
8131
8617
  capabilities: capabilitiesPayload()
@@ -8140,7 +8626,10 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
8140
8626
  }
8141
8627
  dispatcher(control);
8142
8628
  });
8143
- await kandan.join(topic, joinPayload(), { rejoinPayload: joinPayload });
8629
+ const joinResponse = await kandan.join(topic, joinPayload(), {
8630
+ rejoinPayload: joinPayload
8631
+ });
8632
+ const replacedRunners = replacementRunnerSummaries(objectValue(joinResponse)?.replaced_runners);
8144
8633
  const started = options.codexUrl === undefined ? await startOwnedCodexAppServer(options) : undefined;
8145
8634
  if (started !== undefined) {
8146
8635
  cleanup.actions.push(() => {
@@ -8151,7 +8640,7 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
8151
8640
  if (codexUrl === undefined) {
8152
8641
  throw new Error("missing codex app-server websocket URL");
8153
8642
  }
8154
- const instanceId = `codex-${randomUUID2()}`;
8643
+ const instanceId = `codex-${randomUUID3()}`;
8155
8644
  const publishLocalEditorStatus = (payload) => {
8156
8645
  kandan.push(topic, "local_editor_status", payload).catch((error) => {
8157
8646
  log("kandan.local_editor_status_push_failed", {
@@ -8199,6 +8688,7 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
8199
8688
  const runtimeDefaults = runnerRuntimeDefaults(options);
8200
8689
  const instancePayload = {
8201
8690
  instanceId,
8691
+ clientId,
8202
8692
  codexUrl,
8203
8693
  tuiLaunched: options.launchTui,
8204
8694
  cwd: options.cwd,
@@ -8211,7 +8701,15 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
8211
8701
  fast: options.fast ?? false
8212
8702
  };
8213
8703
  await kandan.push(topic, "instance_started", instancePayload);
8214
- log("runner.instance_started", { instanceId, codexUrl });
8704
+ log("runner.instance_started", {
8705
+ runnerId: options.runnerId,
8706
+ hostname: runnerHost,
8707
+ workspace: runnerWorkspaceSlug(options) ?? null,
8708
+ version: linzumiCliVersion,
8709
+ instanceId,
8710
+ codexUrl,
8711
+ replacedRunners
8712
+ });
8215
8713
  const channelSession = options.channelSession === undefined ? undefined : await attachChannelSession({
8216
8714
  kandan,
8217
8715
  codex,
@@ -8312,6 +8810,7 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
8312
8810
  };
8313
8811
  const heartbeatPayload = () => ({
8314
8812
  instanceId,
8813
+ clientId,
8315
8814
  codexUrl,
8316
8815
  cwd: options.cwd,
8317
8816
  hostname: runnerHost,
@@ -8390,6 +8889,20 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
8390
8889
  });
8391
8890
  const handleControl = (control) => {
8392
8891
  log("kandan.control", { control });
8892
+ if (control.type === "replace_runner") {
8893
+ log("runner.replaced", {
8894
+ runnerId: options.runnerId,
8895
+ reason: control.reason,
8896
+ replacementRunnerId: control.replacementRunnerId,
8897
+ replacementVersion: control.replacementVersion
8898
+ });
8899
+ close().catch((error) => {
8900
+ log("runner.replace_close_failed", {
8901
+ message: error instanceof Error ? error.message : String(error)
8902
+ });
8903
+ });
8904
+ return;
8905
+ }
8393
8906
  if (!controlTargetsInstance(control, instanceId)) {
8394
8907
  log("kandan.control_ignored", {
8395
8908
  reason: "instance_id_mismatch",
@@ -8604,8 +9117,24 @@ function normalizedWorkDescription(value) {
8604
9117
  const normalized = value?.trim();
8605
9118
  return normalized === undefined || normalized === "" ? undefined : normalized;
8606
9119
  }
9120
+ function replacementRunnerSummaries(value) {
9121
+ const entries = arrayValue(value) ?? [];
9122
+ return entries.flatMap((entry) => {
9123
+ const record = objectValue(entry);
9124
+ const runnerId = stringValue(record?.runnerId);
9125
+ if (runnerId === undefined) {
9126
+ return [];
9127
+ }
9128
+ return [
9129
+ {
9130
+ runnerId,
9131
+ version: stringValue(record?.version) ?? null
9132
+ }
9133
+ ];
9134
+ });
9135
+ }
8607
9136
  function makeRunnerLogger(options) {
8608
- return createRunnerLogger(options.logFile ?? join6(options.cwd, ".linzumi-runner.log"), options.launchTui ? undefined : reportRunnerConsoleEvent);
9137
+ return createRunnerLogger(options.logFile ?? join8(options.cwd, ".linzumi-runner.log"), options.launchTui ? undefined : reportRunnerConsoleEvent);
8609
9138
  }
8610
9139
  function installCleanupHandlers(close) {
8611
9140
  const closeAndExit = () => {
@@ -9054,11 +9583,12 @@ function optionalThreadControlField(control, field) {
9054
9583
  }
9055
9584
  function startInstanceRuntimeSettings(options, control) {
9056
9585
  const defaults = runnerRuntimeDefaults(options);
9586
+ const sandbox = control.sandbox ?? defaults.sandbox;
9057
9587
  return {
9058
9588
  model: control.model ?? defaults.model,
9059
9589
  reasoningEffort: control.reasoningEffort ?? defaults.reasoningEffort,
9060
- approvalPolicy: control.approvalPolicy ?? defaults.approvalPolicy,
9061
- sandbox: control.sandbox ?? defaults.sandbox,
9590
+ approvalPolicy: codexApprovalPolicyForRequest(control.approvalPolicy ?? defaults.approvalPolicy, sandbox),
9591
+ sandbox,
9062
9592
  fast: control.fast ?? options.fast
9063
9593
  };
9064
9594
  }
@@ -9077,11 +9607,12 @@ function runnerWorkspaceSlug(options) {
9077
9607
  function runnerRuntimeDefaults(options) {
9078
9608
  const session = options.channelSession;
9079
9609
  const defaults = options.runtimeDefaults;
9610
+ const sandbox = defaults?.sandbox ?? session?.sandbox;
9080
9611
  return {
9081
9612
  model: defaults?.model ?? session?.model,
9082
9613
  reasoningEffort: defaults?.reasoningEffort ?? session?.reasoningEffort,
9083
- approvalPolicy: defaults?.approvalPolicy ?? session?.approvalPolicy,
9084
- sandbox: defaults?.sandbox ?? session?.sandbox
9614
+ approvalPolicy: codexApprovalPolicySetting(defaults?.approvalPolicy ?? session?.approvalPolicy, sandbox),
9615
+ sandbox
9085
9616
  };
9086
9617
  }
9087
9618
  function isUpdateRunnerConfigControl(control) {
@@ -9100,9 +9631,9 @@ function configuredAllowedCwds(values) {
9100
9631
  const allowedCwds = [];
9101
9632
  const missingAllowedCwds = [];
9102
9633
  for (const value of normalizeAllowedCwds(values)) {
9103
- const absolutePath = resolve5(expandUserPath(value));
9634
+ const absolutePath = resolve6(expandUserPath(value));
9104
9635
  try {
9105
- const realPath = realpathSync4(absolutePath);
9636
+ const realPath = realpathSync5(absolutePath);
9106
9637
  allowedCwds.push(...realPath === absolutePath ? [realPath] : [realPath, absolutePath]);
9107
9638
  } catch (error) {
9108
9639
  if (isMissingAllowedCwdError(error)) {
@@ -9125,18 +9656,18 @@ function allowedCwdSuggestions(cwd, allowedCwds) {
9125
9656
  }
9126
9657
 
9127
9658
  // src/authCache.ts
9128
- import { existsSync as existsSync4, mkdirSync as mkdirSync5, readFileSync as readFileSync3, writeFileSync as writeFileSync4 } from "node:fs";
9129
- import { homedir as homedir5 } from "node:os";
9130
- import { dirname as dirname4, join as join7 } from "node:path";
9659
+ import { existsSync as existsSync6, mkdirSync as mkdirSync7, readFileSync as readFileSync5, writeFileSync as writeFileSync5 } from "node:fs";
9660
+ import { homedir as homedir6 } from "node:os";
9661
+ import { dirname as dirname6, join as join9 } from "node:path";
9131
9662
  function defaultAuthFilePath() {
9132
- const base = process.env.KANDAN_HOME ?? join7(homedir5(), ".kandan");
9133
- return join7(base, "auth.json");
9663
+ const base = process.env.KANDAN_HOME ?? join9(homedir6(), ".kandan");
9664
+ return join9(base, "auth.json");
9134
9665
  }
9135
9666
  function readCachedLocalRunnerToken(kandanUrl, authFilePath = defaultAuthFilePath()) {
9136
- if (!existsSync4(authFilePath)) {
9667
+ if (!existsSync6(authFilePath)) {
9137
9668
  return;
9138
9669
  }
9139
- const authFile = parseAuthFile(readFileSync3(authFilePath, "utf8"));
9670
+ const authFile = parseAuthFile(readFileSync5(authFilePath, "utf8"));
9140
9671
  const kandanBaseUrl = kandanHttpBaseUrl(kandanUrl);
9141
9672
  const entry = authFile.local_codex_runner?.[kandanBaseUrl];
9142
9673
  if (entry === undefined || entry.access_token.trim() === "") {
@@ -9154,7 +9685,7 @@ function readCachedLocalRunnerToken(kandanUrl, authFilePath = defaultAuthFilePat
9154
9685
  }
9155
9686
  function writeCachedLocalRunnerToken(args) {
9156
9687
  const authFilePath = args.authFilePath ?? defaultAuthFilePath();
9157
- const existing = existsSync4(authFilePath) ? parseAuthFile(readFileSync3(authFilePath, "utf8")) : { version: 1 };
9688
+ const existing = existsSync6(authFilePath) ? parseAuthFile(readFileSync5(authFilePath, "utf8")) : { version: 1 };
9158
9689
  const kandanBaseUrl = kandanHttpBaseUrl(args.kandanUrl);
9159
9690
  const issuedAt = new Date;
9160
9691
  const expiresAt = args.expiresInSeconds === undefined ? undefined : new Date(issuedAt.getTime() + args.expiresInSeconds * 1000).toISOString();
@@ -9170,8 +9701,8 @@ function writeCachedLocalRunnerToken(args) {
9170
9701
  }
9171
9702
  }
9172
9703
  };
9173
- mkdirSync5(dirname4(authFilePath), { recursive: true });
9174
- writeFileSync4(authFilePath, `${JSON.stringify(next, null, 2)}
9704
+ mkdirSync7(dirname6(authFilePath), { recursive: true });
9705
+ writeFileSync5(authFilePath, `${JSON.stringify(next, null, 2)}
9175
9706
  `, "utf8");
9176
9707
  return {
9177
9708
  accessToken: args.accessToken,
@@ -9263,102 +9794,12 @@ async function acquireAndCacheToken(args) {
9263
9794
  return token.accessToken;
9264
9795
  }
9265
9796
 
9266
- // src/localConfig.ts
9267
- import {
9268
- existsSync as existsSync5,
9269
- mkdirSync as mkdirSync6,
9270
- readFileSync as readFileSync4,
9271
- realpathSync as realpathSync5,
9272
- writeFileSync as writeFileSync5
9273
- } from "node:fs";
9274
- import { homedir as homedir6 } from "node:os";
9275
- import { dirname as dirname5, resolve as resolve6 } from "node:path";
9276
- function localConfigPath(env = process.env) {
9277
- const override = env.LINZUMI_CONFIG_FILE;
9278
- return override !== undefined && override.trim() !== "" ? resolve6(expandUserPath(override)) : resolve6(homedir6(), ".linzumi", "config.json");
9279
- }
9280
- function readLocalConfig(path = localConfigPath()) {
9281
- if (!existsSync5(path)) {
9282
- return { version: 1, allowedCwds: [] };
9283
- }
9284
- const parsed = JSON.parse(readFileSync4(path, "utf8"));
9285
- if (!isConfigPayload(parsed)) {
9286
- throw new Error(`invalid Linzumi config: ${path}`);
9287
- }
9288
- return {
9289
- version: 1,
9290
- allowedCwds: uniqueStrings(parsed.allowedCwds)
9291
- };
9292
- }
9293
- function readConfiguredAllowedCwdDetails(path = localConfigPath()) {
9294
- const allowedCwds = [];
9295
- const missingAllowedCwds = [];
9296
- for (const cwd of readLocalConfig(path).allowedCwds) {
9297
- const absolutePath = resolve6(expandUserPath(cwd));
9298
- try {
9299
- const realPath = realpathSync5(absolutePath);
9300
- allowedCwds.push(...realPath === absolutePath ? [realPath] : [realPath, absolutePath]);
9301
- } catch (error) {
9302
- if (isMissingPathError(error)) {
9303
- missingAllowedCwds.push(absolutePath);
9304
- continue;
9305
- }
9306
- throw error;
9307
- }
9308
- }
9309
- return {
9310
- allowedCwds: uniqueStrings(allowedCwds),
9311
- missingAllowedCwds: uniqueStrings(missingAllowedCwds)
9312
- };
9313
- }
9314
- function addAllowedCwd(pathValue, path = localConfigPath()) {
9315
- const normalizedPath = realpathSync5(resolve6(expandUserPath(pathValue)));
9316
- const config = readLocalConfig(path);
9317
- const allowedCwds = uniqueStrings([...config.allowedCwds, normalizedPath]);
9318
- writeLocalConfig({ version: 1, allowedCwds }, path);
9319
- return allowedCwds;
9320
- }
9321
- function removeAllowedCwd(pathValue, path = localConfigPath()) {
9322
- const requestedPath = resolve6(expandUserPath(pathValue));
9323
- const normalizedRequest = realpathOrResolved(requestedPath);
9324
- const config = readLocalConfig(path);
9325
- const allowedCwds = config.allowedCwds.filter((cwd) => {
9326
- const normalizedExisting = realpathOrResolved(cwd);
9327
- return cwd !== pathValue && normalizedExisting !== normalizedRequest;
9328
- });
9329
- writeLocalConfig({ version: 1, allowedCwds }, path);
9330
- return allowedCwds;
9331
- }
9332
- function writeLocalConfig(config, path = localConfigPath()) {
9333
- mkdirSync6(dirname5(path), { recursive: true });
9334
- writeFileSync5(path, `${JSON.stringify(config, null, 2)}
9335
- `, "utf8");
9336
- }
9337
- function isConfigPayload(value) {
9338
- return typeof value === "object" && value !== null && value.version === 1 && Array.isArray(value.allowedCwds) && value.allowedCwds.every((cwd) => typeof cwd === "string" && cwd.trim() !== "");
9339
- }
9340
- function uniqueStrings(values) {
9341
- return [
9342
- ...new Set(values.map((value) => value.trim()).filter((value) => value !== ""))
9343
- ];
9344
- }
9345
- function isMissingPathError(error) {
9346
- return typeof error === "object" && error !== null && "code" in error && (error.code === "ENOENT" || error.code === "ENOTDIR" || error.code === "EACCES" || error.code === "ELOOP" || error.code === "EIO");
9347
- }
9348
- function realpathOrResolved(pathValue) {
9349
- try {
9350
- return realpathSync5(resolve6(expandUserPath(pathValue)));
9351
- } catch (_error) {
9352
- return resolve6(expandUserPath(pathValue));
9353
- }
9354
- }
9355
-
9356
9797
  // src/defaultUrls.ts
9357
9798
  var defaultLinzumiHttpUrl = "https://serve.linzumi.com";
9358
9799
  var defaultLinzumiWebSocketUrl = "wss://serve.linzumi.com";
9359
9800
 
9360
9801
  // src/kandanTls.ts
9361
- import { existsSync as existsSync6, readFileSync as readFileSync5 } from "node:fs";
9802
+ import { existsSync as existsSync7, readFileSync as readFileSync6 } from "node:fs";
9362
9803
  import { Agent } from "undici";
9363
9804
  import { WebSocket as WsWebSocket } from "ws";
9364
9805
  function kandanTlsTrustFromEnv() {
@@ -9369,10 +9810,10 @@ function kandanTlsTrustFromCaFile(caFile) {
9369
9810
  return;
9370
9811
  }
9371
9812
  const trimmed = caFile.trim();
9372
- if (!existsSync6(trimmed)) {
9813
+ if (!existsSync7(trimmed)) {
9373
9814
  throw new Error(`KANDAN_TLS_CA_FILE does not exist: ${trimmed}`);
9374
9815
  }
9375
- const ca = readFileSync5(trimmed, "utf8");
9816
+ const ca = readFileSync6(trimmed, "utf8");
9376
9817
  return {
9377
9818
  caFile: trimmed,
9378
9819
  ca,
@@ -9401,8 +9842,8 @@ function trustedWebSocketFactory(trust, WebSocketImpl = WsWebSocket) {
9401
9842
  }
9402
9843
 
9403
9844
  // src/agentBootstrap.ts
9404
- import { existsSync as existsSync7, mkdirSync as mkdirSync7, readFileSync as readFileSync6, writeFileSync as writeFileSync6 } from "node:fs";
9405
- import { dirname as dirname6, join as join8 } from "node:path";
9845
+ import { existsSync as existsSync8, mkdirSync as mkdirSync8, readFileSync as readFileSync7, writeFileSync as writeFileSync6 } from "node:fs";
9846
+ import { dirname as dirname7, join as join10 } from "node:path";
9406
9847
  import { homedir as homedir7 } from "node:os";
9407
9848
  async function runAgentCliCommand(args, deps = {
9408
9849
  fetchImpl: fetch,
@@ -10002,7 +10443,7 @@ function agentTokenFile(flags) {
10002
10443
  return flags.get("agent-token-file") ?? defaultAgentTokenFilePath();
10003
10444
  }
10004
10445
  function defaultAgentTokenFilePath() {
10005
- return join8(homedir7(), ".linzumi", "agent-token.json");
10446
+ return join10(homedir7(), ".linzumi", "agent-token.json");
10006
10447
  }
10007
10448
  function normalizedApiUrl(apiUrl) {
10008
10449
  return apiUrl.endsWith("/") ? apiUrl : `${apiUrl}/`;
@@ -10011,10 +10452,10 @@ function authorizationHeaders(token) {
10011
10452
  return { authorization: `Bearer ${token}` };
10012
10453
  }
10013
10454
  function readOptionalTextFile(path) {
10014
- return existsSync7(path) ? readFileSync6(path, "utf8") : undefined;
10455
+ return existsSync8(path) ? readFileSync7(path, "utf8") : undefined;
10015
10456
  }
10016
10457
  function writeTextFile(path, content) {
10017
- mkdirSync7(dirname6(path), { recursive: true });
10458
+ mkdirSync8(dirname7(path), { recursive: true });
10018
10459
  writeFileSync6(path, content);
10019
10460
  }
10020
10461
  function readStoredAgentTokenFile(path, readTextFile = readOptionalTextFile) {
@@ -10091,8 +10532,8 @@ Launch target:
10091
10532
  }
10092
10533
 
10093
10534
  // src/helloLinzumiProject.ts
10094
- import { existsSync as existsSync8, mkdirSync as mkdirSync8, readFileSync as readFileSync7, rmSync as rmSync2, writeFileSync as writeFileSync7 } from "node:fs";
10095
- import { dirname as dirname7, join as join9, resolve as resolve7 } from "node:path";
10535
+ import { existsSync as existsSync9, mkdirSync as mkdirSync9, readFileSync as readFileSync8, rmSync as rmSync2, writeFileSync as writeFileSync7 } from "node:fs";
10536
+ import { dirname as dirname8, join as join11, resolve as resolve7 } from "node:path";
10096
10537
  import { fileURLToPath } from "node:url";
10097
10538
  var defaultHelloLinzumiProjectDir = "/tmp/hello_linzumi";
10098
10539
  var defaultHelloLinzumiProjectName = "hello_linzumi";
@@ -10100,8 +10541,8 @@ var defaultHelloLinzumiParentDir = "/tmp";
10100
10541
  var defaultHelloLinzumiPort = 8787;
10101
10542
  var defaultHelloLinzumiHost = "0.0.0.0";
10102
10543
  var markerFile = ".linzumi-demo-project";
10103
- var moduleDir = dirname7(fileURLToPath(import.meta.url));
10104
- var linzumiLogoSvg = readFileSync7(join9(moduleDir, "assets", "linzumi-logo.svg"), "utf8");
10544
+ var moduleDir = dirname8(fileURLToPath(import.meta.url));
10545
+ var linzumiLogoSvg = readFileSync8(join11(moduleDir, "assets", "linzumi-logo.svg"), "utf8");
10105
10546
  function createHelloLinzumiProject(input = {}) {
10106
10547
  const options = typeof input === "string" ? { rootPath: input } : input;
10107
10548
  const root = resolveHelloProjectRoot(options);
@@ -10109,9 +10550,9 @@ function createHelloLinzumiProject(input = {}) {
10109
10550
  const host = normalizeHost(options.host);
10110
10551
  assertTcpPort(port);
10111
10552
  assertWritableDemoRoot(root, options.reset === true);
10112
- mkdirSync8(join9(root, "src"), { recursive: true });
10553
+ mkdirSync9(join11(root, "src"), { recursive: true });
10113
10554
  for (const file of demoFiles({ root, port, host })) {
10114
- writeFileSync7(join9(root, file.path), file.content, "utf8");
10555
+ writeFileSync7(join11(root, file.path), file.content, "utf8");
10115
10556
  }
10116
10557
  return {
10117
10558
  root,
@@ -10152,11 +10593,11 @@ function assertTcpPort(port) {
10152
10593
  throw new Error("--port must be a TCP port from 1 to 65535");
10153
10594
  }
10154
10595
  function assertWritableDemoRoot(root, reset) {
10155
- if (!existsSync8(root)) {
10596
+ if (!existsSync9(root)) {
10156
10597
  return;
10157
10598
  }
10158
- const markerPath = join9(root, markerFile);
10159
- const isDemoRoot = existsSync8(markerPath) && readFileSync7(markerPath, "utf8").trim() === "hello-linzumi";
10599
+ const markerPath = join11(root, markerFile);
10600
+ const isDemoRoot = existsSync9(markerPath) && readFileSync8(markerPath, "utf8").trim() === "hello-linzumi";
10160
10601
  if (isDemoRoot && reset) {
10161
10602
  rmSync2(root, { recursive: true, force: true });
10162
10603
  return;
@@ -10655,27 +11096,30 @@ To kick the agent off:
10655
11096
 
10656
11097
  // src/commanderDaemon.ts
10657
11098
  import {
10658
- existsSync as existsSync9,
10659
- closeSync,
10660
- mkdirSync as mkdirSync9,
10661
- openSync as openSync2,
10662
- readFileSync as readFileSync8,
11099
+ existsSync as existsSync10,
11100
+ closeSync as closeSync2,
11101
+ mkdirSync as mkdirSync10,
11102
+ openSync as openSync3,
11103
+ readFileSync as readFileSync9,
10663
11104
  watch,
10664
11105
  writeFileSync as writeFileSync8
10665
11106
  } from "node:fs";
10666
11107
  import { homedir as homedir8 } from "node:os";
10667
- import { dirname as dirname8, join as join10, resolve as resolve8 } from "node:path";
11108
+ import { dirname as dirname9, join as join12, resolve as resolve8 } from "node:path";
10668
11109
  import { execFileSync, spawn as spawn7 } from "node:child_process";
10669
11110
  import { fileURLToPath as fileURLToPath2 } from "node:url";
10670
- var connectedMarker = "Runner connected:";
11111
+ var connectedMarkers = ["Connected to Linzumi", "Runner connected:"];
10671
11112
  function commanderStatusDir() {
10672
- return join10(homedir8(), ".linzumi", "commanders");
11113
+ return join12(homedir8(), ".linzumi", "commanders");
10673
11114
  }
10674
11115
  function commanderStatusFile(runnerId, statusDir = commanderStatusDir()) {
10675
- return join10(statusDir, `${safeRunnerId(runnerId)}.json`);
11116
+ return join12(statusDir, `${safeRunnerId(runnerId)}.json`);
10676
11117
  }
10677
11118
  function defaultCommanderLogFile(runnerId, statusDir = commanderStatusDir()) {
10678
- return join10(statusDir, `${safeRunnerId(runnerId)}.log`);
11119
+ return join12(statusDir, `${safeRunnerId(runnerId)}.log`);
11120
+ }
11121
+ function commanderLogIsConnected(log) {
11122
+ return connectedMarkers.some((marker) => log.includes(marker));
10679
11123
  }
10680
11124
  function startCommanderDaemon(options) {
10681
11125
  const statusDir = options.statusDir ?? commanderStatusDir();
@@ -10693,10 +11137,10 @@ function startCommanderDaemon(options) {
10693
11137
  "--log-file",
10694
11138
  logFile
10695
11139
  ];
10696
- mkdirSync9(statusDir, { recursive: true });
10697
- mkdirSync9(dirname8(logFile), { recursive: true });
10698
- const out = openSync2(logFile, "a");
10699
- const err = openSync2(logFile, "a");
11140
+ mkdirSync10(statusDir, { recursive: true });
11141
+ mkdirSync10(dirname9(logFile), { recursive: true });
11142
+ const out = openSync3(logFile, "a");
11143
+ const err = openSync3(logFile, "a");
10700
11144
  writeCliAuditEvent("process.spawn", {
10701
11145
  command: nodeBin,
10702
11146
  args: command,
@@ -10715,8 +11159,8 @@ function startCommanderDaemon(options) {
10715
11159
  pid: child.pid,
10716
11160
  purpose: "commander_daemon.start"
10717
11161
  }, { sessionId: options.runnerId });
10718
- closeSync(out);
10719
- closeSync(err);
11162
+ closeSync2(out);
11163
+ closeSync2(err);
10720
11164
  child.unref();
10721
11165
  if (child.pid === undefined) {
10722
11166
  throw new Error("commander daemon did not report a pid");
@@ -10738,15 +11182,15 @@ function startCommanderDaemon(options) {
10738
11182
  }
10739
11183
  function commanderDaemonStatus(runnerId, statusDir = commanderStatusDir(), processIdentityReader = readProcessIdentity) {
10740
11184
  const statusFile = commanderStatusFile(runnerId, statusDir);
10741
- if (!existsSync9(statusFile)) {
11185
+ if (!existsSync10(statusFile)) {
10742
11186
  return { status: "missing", runnerId, statusFile };
10743
11187
  }
10744
- const record = parseRecord(readFileSync8(statusFile, "utf8"));
11188
+ const record = parseRecord(readFileSync9(statusFile, "utf8"));
10745
11189
  return processIsRunning(record.pid) && processMatchesRecord(record, processIdentityReader) ? { status: "running", record } : { status: "stopped", record };
10746
11190
  }
10747
11191
  async function waitForCommanderDaemon(options) {
10748
11192
  const now = options.now ?? (() => Date.now());
10749
- const readTextFile = options.readTextFile ?? ((path) => existsSync9(path) ? readFileSync8(path, "utf8") : undefined);
11193
+ const readTextFile = options.readTextFile ?? ((path) => existsSync10(path) ? readFileSync9(path, "utf8") : undefined);
10750
11194
  const statusImpl = options.statusImpl ?? commanderDaemonStatus;
10751
11195
  const deadline = now() + options.timeoutMs;
10752
11196
  while (now() <= deadline) {
@@ -10761,12 +11205,12 @@ async function waitForCommanderDaemon(options) {
10761
11205
  if (log === undefined) {
10762
11206
  return { ok: false, reason: "timeout" };
10763
11207
  }
10764
- if (log.includes(connectedMarker)) {
11208
+ if (commanderLogIsConnected(log)) {
10765
11209
  return { ok: true, record: status.record };
10766
11210
  }
10767
11211
  await waitForFileChangeOrTimeout(status.record.logFile, deadline, now, () => {
10768
11212
  const updatedLog = readTextFile(status.record.logFile);
10769
- return updatedLog !== undefined && updatedLog.includes(connectedMarker);
11213
+ return updatedLog !== undefined && commanderLogIsConnected(updatedLog);
10770
11214
  });
10771
11215
  }
10772
11216
  }
@@ -10976,7 +11420,7 @@ async function main(args) {
10976
11420
  process.stdout.write(connectGuideText());
10977
11421
  return;
10978
11422
  case "version":
10979
- process.stdout.write(`linzumi 0.0.41-beta
11423
+ process.stdout.write(`${linzumiCliVersionText}
10980
11424
  `);
10981
11425
  return;
10982
11426
  case "auth":
@@ -10996,18 +11440,18 @@ async function main(args) {
10996
11440
  return;
10997
11441
  case "agentRunner": {
10998
11442
  const options = await parseAgentRunnerArgs(parsed.args);
10999
- await runLocalCodexRunner(options);
11443
+ await runLocalCodexRunner(withLocalMachineId(options));
11000
11444
  return;
11001
11445
  }
11002
11446
  case "start": {
11003
11447
  const options = await parseStartRunnerArgs(parsed.args);
11004
11448
  addAllowedCwd(options.cwd);
11005
- await runLocalCodexRunner(options);
11449
+ await runLocalCodexRunner(withLocalMachineId(options));
11006
11450
  return;
11007
11451
  }
11008
11452
  case "run": {
11009
11453
  const options = await parseRunnerArgs(parsed.args);
11010
- await runLocalCodexRunner(options);
11454
+ await runLocalCodexRunner(withLocalMachineId(options));
11011
11455
  return;
11012
11456
  }
11013
11457
  }
@@ -11318,7 +11762,7 @@ async function parseStartRunnerArgs(args, deps = {
11318
11762
  return {
11319
11763
  kandanUrl,
11320
11764
  token: targetToken,
11321
- runnerId: stringValue3(values, "runner-id") ?? `runner-${randomUUID3()}`,
11765
+ runnerId: stringValue3(values, "runner-id") ?? `runner-${randomUUID4()}`,
11322
11766
  workspaceSlug: target.workspaceSlug,
11323
11767
  cwd,
11324
11768
  codexBin,
@@ -11406,7 +11850,7 @@ async function parseAgentRunnerArgs(args, deps = {
11406
11850
  return {
11407
11851
  kandanUrl,
11408
11852
  token: tokenFile.commanderToken,
11409
- runnerId: stringValue3(values, "runner-id") ?? `agent-runner-${randomUUID3()}`,
11853
+ runnerId: stringValue3(values, "runner-id") ?? `agent-runner-${randomUUID4()}`,
11410
11854
  workspaceSlug: tokenFile.workspaceId,
11411
11855
  cwd,
11412
11856
  codexBin,
@@ -11426,7 +11870,7 @@ async function parseAgentRunnerArgs(args, deps = {
11426
11870
  };
11427
11871
  }
11428
11872
  function readAgentTokenTextFile(path) {
11429
- return existsSync10(path) ? readFileSync9(path, "utf8") : undefined;
11873
+ return existsSync11(path) ? readFileSync10(path, "utf8") : undefined;
11430
11874
  }
11431
11875
  function rejectAgentRunnerTargetingFlags(values) {
11432
11876
  const unsupportedFlags = [
@@ -11502,12 +11946,12 @@ async function parseRunnerArgs(args, deps = {
11502
11946
  process.exit(0);
11503
11947
  }
11504
11948
  if (values.get("version") === true) {
11505
- process.stdout.write(`linzumi 0.0.41-beta
11949
+ process.stdout.write(`${linzumiCliVersionText}
11506
11950
  `);
11507
11951
  process.exit(0);
11508
11952
  }
11509
- const channelTarget = parseChannelSessionTarget(values);
11510
- const kandanUrl = required(values, "linzumi-url");
11953
+ const connectTarget = parseConnectTarget(values);
11954
+ const kandanUrl = stringValue3(values, "linzumi-url") ?? defaultLinzumiWebSocketUrl;
11511
11955
  const cwd = stringValue3(values, "cwd") ?? process.cwd();
11512
11956
  const cwdAllowedCwds = assertConfiguredAllowedCwds([cwd]);
11513
11957
  const localConfiguredAllowedCwds = values.has("allowed-cwd") ? { allowedCwds: [], missingAllowedCwds: [] } : readConfiguredAllowedCwdDetails();
@@ -11518,8 +11962,8 @@ async function parseRunnerArgs(args, deps = {
11518
11962
  const token = await deps.resolveToken({
11519
11963
  kandanUrl,
11520
11964
  explicitToken,
11521
- workspaceSlug: channelTarget?.workspaceSlug,
11522
- channelSlug: channelTarget?.channelSlug,
11965
+ workspaceSlug: connectTarget?.workspaceSlug,
11966
+ channelSlug: connectTarget?.channelSlug,
11523
11967
  authFilePath: stringValue3(values, "auth-file"),
11524
11968
  callbackHost: stringValue3(values, "oauth-callback-host"),
11525
11969
  reportRejectedCachedToken: () => {
@@ -11527,7 +11971,7 @@ async function parseRunnerArgs(args, deps = {
11527
11971
  `);
11528
11972
  }
11529
11973
  });
11530
- const channelSession = parseChannelSession(values, token, channelTarget);
11974
+ const channelSession = parseChannelSession(values, token, connectTarget);
11531
11975
  const editorRuntime = await deps.resolveEditorRuntime({
11532
11976
  kandanUrl,
11533
11977
  token,
@@ -11544,7 +11988,7 @@ async function parseRunnerArgs(args, deps = {
11544
11988
  return {
11545
11989
  kandanUrl,
11546
11990
  token,
11547
- runnerId: stringValue3(values, "runner-id") ?? `runner-${randomUUID3()}`,
11991
+ runnerId: stringValue3(values, "runner-id") ?? `runner-${randomUUID4()}`,
11548
11992
  cwd,
11549
11993
  codexBin,
11550
11994
  codexUrl: stringValue3(values, "codex-url"),
@@ -11558,7 +12002,7 @@ async function parseRunnerArgs(args, deps = {
11558
12002
  editorRuntime: editorRuntime.runtime,
11559
12003
  socketFactory: trustedWebSocketFactory(kandanTlsTrustFromEnv()),
11560
12004
  dependencyStatus,
11561
- workspaceSlug: channelSession?.workspaceSlug ?? singleWorkspaceScopeFromAccessToken(token),
12005
+ workspaceSlug: connectTarget?.workspaceSlug ?? singleWorkspaceScopeFromAccessToken(token),
11562
12006
  runtimeDefaults: runnerRuntimeDefaultsFromValues(values),
11563
12007
  channelSession
11564
12008
  };
@@ -11670,7 +12114,7 @@ function resolveUserPath(pathValue) {
11670
12114
  return resolve9(pathValue);
11671
12115
  }
11672
12116
  function parseChannelSession(values, token, target) {
11673
- if (target === undefined) {
12117
+ if (target === undefined || target.channelSlug === undefined) {
11674
12118
  return;
11675
12119
  }
11676
12120
  const listenUser = stringValue3(values, "listen-user") ?? defaultListenUserFromToken(token);
@@ -11686,7 +12130,7 @@ function parseChannelSession(values, token, target) {
11686
12130
  streamFlushMs: positiveIntegerValue(values, "stream-flush-ms")
11687
12131
  };
11688
12132
  }
11689
- function parseChannelSessionTarget(values) {
12133
+ function parseConnectTarget(values) {
11690
12134
  return parseOptionalChannelTarget(values);
11691
12135
  }
11692
12136
  function defaultListenUserFromToken(token) {
@@ -11699,12 +12143,15 @@ function defaultListenUserFromToken(token) {
11699
12143
  function parseOptionalChannelTarget(values) {
11700
12144
  const channel = stringValue3(values, "channel");
11701
12145
  const workspace = stringValue3(values, "workspace");
11702
- if (channel === undefined && workspace === undefined) {
11703
- return;
12146
+ if (channel === undefined) {
12147
+ if (workspace === undefined) {
12148
+ return;
12149
+ }
12150
+ return { workspaceSlug: workspace };
11704
12151
  }
11705
- return channel !== undefined && channel.includes("/") ? parseChannelPath(channel) : {
12152
+ return channel.includes("/") ? parseChannelPath(channel) : {
11706
12153
  workspaceSlug: workspace ?? required(values, "workspace"),
11707
- channelSlug: channel ?? required(values, "channel")
12154
+ channelSlug: channel
11708
12155
  };
11709
12156
  }
11710
12157
  function parseChannelPath(channel) {
@@ -11717,6 +12164,12 @@ function parseChannelPath(channel) {
11717
12164
  channelSlug: channelSlug.trim()
11718
12165
  };
11719
12166
  }
12167
+ function withLocalMachineId(options) {
12168
+ return {
12169
+ ...options,
12170
+ machineId: ensureLocalMachineId()
12171
+ };
12172
+ }
11720
12173
  function required(values, key) {
11721
12174
  const value = stringValue3(values, key);
11722
12175
  if (value === undefined) {
@@ -11773,19 +12226,19 @@ Usage:
11773
12226
  linzumi commander <folder> [options]
11774
12227
  linzumi start <folder> [options]
11775
12228
  linzumi paths list|add|remove [path]
11776
- linzumi connect --linzumi-url <ws-url> --workspace <slug> --channel <slug> [options]
12229
+ linzumi connect --workspace <slug> [--channel <slug>] [options]
11777
12230
  linzumi auth --linzumi-url <ws-url> [--workspace <slug> --channel <slug>]
11778
12231
 
11779
- Required:
12232
+ Connection:
11780
12233
  --linzumi-url <ws-url> Linzumi backend URL, default ${defaultLinzumiWebSocketUrl}
11781
12234
  (deprecated alias: --kandan-url)
11782
12235
  --token <jwt> Optional override token. Otherwise ~/.linzumi/auth.json is validated or OAuth opens.
11783
12236
  --auth-file <path> Auth cache path, default ~/.linzumi/auth.json
11784
12237
  --oauth-callback-host <ip> Callback host reachable by your browser
11785
12238
 
11786
- Channel binding:
12239
+ Workspace and optional channel binding:
11787
12240
  --workspace <slug> Workspace slug
11788
- --channel <slug|w/c> Channel slug, or workspace/channel
12241
+ --channel <slug|w/c> Optional channel slug, or workspace/channel
11789
12242
  --linzumi-thread-id <uuid> Resume an existing Linzumi thread instead of announcing a new root
11790
12243
  (deprecated alias: --kandan-thread-id)
11791
12244
  --listen-user <user|all> User whose replies are accepted, default authenticated user
@@ -11818,8 +12271,9 @@ Examples:
11818
12271
  linzumi commander daemon --runner-id launch-commander
11819
12272
  linzumi start ~/
11820
12273
  linzumi start ~/code/my-app
11821
- linzumi connect --workspace <your-workspace> --channel <your-channel> --launch-tui
11822
- linzumi connect --workspace <your-workspace> --channel <your-channel> --model gpt-5 --reasoning-effort low --fast --launch-tui
12274
+ linzumi connect --workspace <your-workspace> --launch-tui
12275
+ linzumi connect --workspace <your-workspace> --model gpt-5 --reasoning-effort low --fast --launch-tui
12276
+ linzumi connect --workspace <your-workspace> --channel <your-channel>
11823
12277
  linzumi auth --workspace <your-workspace> --channel <your-channel>
11824
12278
  linzumi paths add ~/code/my-app
11825
12279
  linzumi paths list
@@ -11829,9 +12283,9 @@ Examples:
11829
12283
  Missing --listen-user and authenticated user is unavailable.
11830
12284
  linzumi connect --token "$TOKEN" --listen-users sean
11831
12285
  Invalid flag: use --listen-user.
11832
- linzumi connect --workspace <your-workspace> --channel <your-channel> --allowed-cwd /does/not/exist
12286
+ linzumi connect --workspace <your-workspace> --allowed-cwd /does/not/exist
11833
12287
  Invalid --allowed-cwd: allowed cwd roots must exist locally.
11834
- linzumi connect --workspace <your-workspace> --channel <your-channel> --forward-port vite
12288
+ linzumi connect --workspace <your-workspace> --forward-port vite
11835
12289
  Invalid --forward-port: value must be a TCP port from 1 to 65535.
11836
12290
  `;
11837
12291
  }
@@ -11989,7 +12443,7 @@ space, persists this folder to your trusted-paths list, and starts this
11989
12443
  computer as a local Codex runner.
11990
12444
 
11991
12445
  Advanced (when you already know your workspace and channel):
11992
- linzumi connect --workspace <your-workspace> --channel <your-channel>
12446
+ linzumi connect --workspace <your-workspace>
11993
12447
 
11994
12448
  For help:
11995
12449
  linzumi connect --help
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@linzumi/cli",
3
- "version": "0.0.41-beta",
3
+ "version": "0.0.43-beta",
4
4
  "description": "Linzumi CLI — point a Codex agent at the real code on your laptop, with your team watching and steering from shared threads.",
5
5
  "type": "module",
6
6
  "bin": {