@linzumi/cli 0.0.41-beta → 0.0.42-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 +595 -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.42-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":
@@ -3611,7 +3631,7 @@ async function handleCodexServerRequest(args, state, payloadContext, request) {
3611
3631
  throw new Error(message);
3612
3632
  }
3613
3633
  function codexApprovalRequestCanAutoAccept(settings, method) {
3614
- return settings.approvalPolicy === "never" && (method === "item/commandExecution/requestApproval" || method === "item/fileChange/requestApproval");
3634
+ return settings.approvalPolicy === "never" && settings.sandbox === "danger-full-access" && (method === "item/commandExecution/requestApproval" || method === "item/fileChange/requestApproval");
3615
3635
  }
3616
3636
  function codexApprovalRequestCanSurface(method) {
3617
3637
  return method === "item/commandExecution/requestApproval" || method === "item/fileChange/requestApproval";
@@ -5132,20 +5152,27 @@ function runtimeSettingsFromOptions(options) {
5132
5152
  return {
5133
5153
  model: options.channelSession.model,
5134
5154
  reasoningEffort: options.channelSession.reasoningEffort,
5135
- approvalPolicy: options.channelSession.approvalPolicy,
5155
+ approvalPolicy: codexApprovalPolicySetting(options.channelSession.approvalPolicy, options.channelSession.sandbox),
5136
5156
  sandbox: options.channelSession.sandbox,
5137
5157
  fast: options.fast
5138
5158
  };
5139
5159
  }
5140
5160
  function mergeRuntimeSettings(current, update) {
5161
+ const sandbox = mergeOptionalStringRuntimeSetting(current.sandbox, update, "sandbox");
5141
5162
  return {
5142
5163
  model: mergeOptionalStringRuntimeSetting(current.model, update, "model"),
5143
5164
  reasoningEffort: mergeOptionalStringRuntimeSetting(current.reasoningEffort, update, "reasoningEffort"),
5144
- approvalPolicy: mergeOptionalStringRuntimeSetting(current.approvalPolicy, update, "approvalPolicy"),
5145
- sandbox: mergeOptionalStringRuntimeSetting(current.sandbox, update, "sandbox"),
5165
+ approvalPolicy: mergeOptionalApprovalPolicyRuntimeSetting(current.approvalPolicy, update, sandbox),
5166
+ sandbox,
5146
5167
  fast: update.fast ?? current.fast
5147
5168
  };
5148
5169
  }
5170
+ function mergeOptionalApprovalPolicyRuntimeSetting(current, update, sandbox) {
5171
+ if (Object.prototype.hasOwnProperty.call(update, "approvalPolicy")) {
5172
+ return codexApprovalPolicySetting(update.approvalPolicy ?? undefined, sandbox);
5173
+ }
5174
+ return codexApprovalPolicySetting(current, sandbox);
5175
+ }
5149
5176
  function mergeOptionalStringRuntimeSetting(current, update, key) {
5150
5177
  if (Object.prototype.hasOwnProperty.call(update, key)) {
5151
5178
  return update[key] ?? undefined;
@@ -7947,6 +7974,356 @@ function waitForOpen2(websocket) {
7947
7974
  });
7948
7975
  }
7949
7976
 
7977
+ // src/runnerLock.ts
7978
+ import {
7979
+ closeSync,
7980
+ existsSync as existsSync5,
7981
+ mkdirSync as mkdirSync6,
7982
+ openSync as openSync2,
7983
+ readFileSync as readFileSync4,
7984
+ unlinkSync as unlinkSync2,
7985
+ writeSync
7986
+ } from "node:fs";
7987
+ import { dirname as dirname5, join as join7 } from "node:path";
7988
+
7989
+ // src/localConfig.ts
7990
+ import { randomUUID as randomUUID2 } from "node:crypto";
7991
+ import {
7992
+ existsSync as existsSync4,
7993
+ linkSync,
7994
+ mkdirSync as mkdirSync5,
7995
+ readFileSync as readFileSync3,
7996
+ realpathSync as realpathSync4,
7997
+ unlinkSync,
7998
+ writeFileSync as writeFileSync4
7999
+ } from "node:fs";
8000
+ import { homedir as homedir5 } from "node:os";
8001
+ import { basename as basename4, dirname as dirname4, join as join6, resolve as resolve5 } from "node:path";
8002
+ function localConfigPath(env = process.env) {
8003
+ const override = env.LINZUMI_CONFIG_FILE;
8004
+ return override !== undefined && override.trim() !== "" ? resolve5(expandUserPath(override)) : resolve5(homedir5(), ".linzumi", "config.json");
8005
+ }
8006
+ function readLocalConfig(path = localConfigPath()) {
8007
+ if (!existsSync4(path)) {
8008
+ return { version: 1, allowedCwds: [] };
8009
+ }
8010
+ const parsed = JSON.parse(readFileSync3(path, "utf8"));
8011
+ if (!isConfigPayload(parsed)) {
8012
+ throw new Error(`invalid Linzumi config: ${path}`);
8013
+ }
8014
+ const allowedCwds = uniqueStrings(parsed.allowedCwds);
8015
+ return parsed.machineId === undefined ? { version: 1, allowedCwds } : { version: 1, machineId: parsed.machineId, allowedCwds };
8016
+ }
8017
+ function ensureLocalMachineId(path = localConfigPath(), createMachineId = randomUUID2) {
8018
+ const config = readLocalConfig(path);
8019
+ if (config.machineId !== undefined) {
8020
+ return config.machineId;
8021
+ }
8022
+ const machineId = ensureLocalMachineIdSeed(path, createMachineId);
8023
+ const latestConfig = readLocalConfig(path);
8024
+ const latestMachineId = latestConfig.machineId;
8025
+ if (latestMachineId !== undefined) {
8026
+ return latestMachineId;
8027
+ }
8028
+ writeLocalConfig({ ...latestConfig, machineId }, path);
8029
+ return machineId;
8030
+ }
8031
+ function localMachineIdSeedPath(configPath = localConfigPath()) {
8032
+ return join6(dirname4(configPath), `${basename4(configPath)}.machine-id`);
8033
+ }
8034
+ function readConfiguredAllowedCwdDetails(path = localConfigPath()) {
8035
+ const allowedCwds = [];
8036
+ const missingAllowedCwds = [];
8037
+ for (const cwd of readLocalConfig(path).allowedCwds) {
8038
+ const absolutePath = resolve5(expandUserPath(cwd));
8039
+ try {
8040
+ const realPath = realpathSync4(absolutePath);
8041
+ allowedCwds.push(...realPath === absolutePath ? [realPath] : [realPath, absolutePath]);
8042
+ } catch (error) {
8043
+ if (isMissingPathError(error)) {
8044
+ missingAllowedCwds.push(absolutePath);
8045
+ continue;
8046
+ }
8047
+ throw error;
8048
+ }
8049
+ }
8050
+ return {
8051
+ allowedCwds: uniqueStrings(allowedCwds),
8052
+ missingAllowedCwds: uniqueStrings(missingAllowedCwds)
8053
+ };
8054
+ }
8055
+ function addAllowedCwd(pathValue, path = localConfigPath()) {
8056
+ const normalizedPath = realpathSync4(resolve5(expandUserPath(pathValue)));
8057
+ const config = readLocalConfig(path);
8058
+ const allowedCwds = uniqueStrings([...config.allowedCwds, normalizedPath]);
8059
+ writeLocalConfig({ ...config, version: 1, allowedCwds }, path);
8060
+ return allowedCwds;
8061
+ }
8062
+ function removeAllowedCwd(pathValue, path = localConfigPath()) {
8063
+ const requestedPath = resolve5(expandUserPath(pathValue));
8064
+ const normalizedRequest = realpathOrResolved(requestedPath);
8065
+ const config = readLocalConfig(path);
8066
+ const allowedCwds = config.allowedCwds.filter((cwd) => {
8067
+ const normalizedExisting = realpathOrResolved(cwd);
8068
+ return cwd !== pathValue && normalizedExisting !== normalizedRequest;
8069
+ });
8070
+ writeLocalConfig({ ...config, version: 1, allowedCwds }, path);
8071
+ return allowedCwds;
8072
+ }
8073
+ function writeLocalConfig(config, path = localConfigPath()) {
8074
+ mkdirSync5(dirname4(path), { recursive: true });
8075
+ writeFileSync4(path, `${JSON.stringify(config, null, 2)}
8076
+ `, "utf8");
8077
+ }
8078
+ function isConfigPayload(value) {
8079
+ 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() !== "");
8080
+ }
8081
+ function machineIdValid(value) {
8082
+ 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);
8083
+ }
8084
+ function ensureLocalMachineIdSeed(configPath, createMachineId) {
8085
+ const seedPath = localMachineIdSeedPath(configPath);
8086
+ if (existsSync4(seedPath)) {
8087
+ return readMachineIdSeed(seedPath);
8088
+ }
8089
+ const machineId = createMachineId();
8090
+ if (!machineIdValid(machineId)) {
8091
+ throw new Error(`invalid generated Linzumi machine id: ${machineId}`);
8092
+ }
8093
+ mkdirSync5(dirname4(seedPath), { recursive: true });
8094
+ const tempPath = join6(dirname4(seedPath), `.${basename4(seedPath)}.${process.pid}.${randomUUID2()}.tmp`);
8095
+ writeFileSync4(tempPath, `${machineId}
8096
+ `, { encoding: "utf8", flag: "wx" });
8097
+ try {
8098
+ linkSync(tempPath, seedPath);
8099
+ return machineId;
8100
+ } catch (error) {
8101
+ if (isNodeErrorCode(error, "EEXIST")) {
8102
+ return readMachineIdSeed(seedPath);
8103
+ }
8104
+ throw error;
8105
+ } finally {
8106
+ unlinkSync(tempPath);
8107
+ }
8108
+ }
8109
+ function readMachineIdSeed(seedPath) {
8110
+ const machineId = readFileSync3(seedPath, "utf8").trim();
8111
+ if (!machineIdValid(machineId)) {
8112
+ throw new Error(`invalid Linzumi machine id seed: ${seedPath}`);
8113
+ }
8114
+ return machineId;
8115
+ }
8116
+ function uniqueStrings(values) {
8117
+ return [
8118
+ ...new Set(values.map((value) => value.trim()).filter((value) => value !== ""))
8119
+ ];
8120
+ }
8121
+ function isMissingPathError(error) {
8122
+ return typeof error === "object" && error !== null && "code" in error && (error.code === "ENOENT" || error.code === "ENOTDIR" || error.code === "EACCES" || error.code === "ELOOP" || error.code === "EIO");
8123
+ }
8124
+ function isNodeErrorCode(error, code) {
8125
+ return typeof error === "object" && error !== null && "code" in error && error.code === code;
8126
+ }
8127
+ function realpathOrResolved(pathValue) {
8128
+ try {
8129
+ return realpathSync4(resolve5(expandUserPath(pathValue)));
8130
+ } catch (_error) {
8131
+ return resolve5(expandUserPath(pathValue));
8132
+ }
8133
+ }
8134
+
8135
+ // src/version.ts
8136
+ var linzumiCliVersion = "0.0.42-beta";
8137
+ var linzumiCliVersionText = `linzumi ${linzumiCliVersion}`;
8138
+
8139
+ // src/runnerLock.ts
8140
+ function runnerLockPath(machineId, configPath = localConfigPath()) {
8141
+ return join7(dirname5(configPath), "runners", `${encodeURIComponent(machineId)}.lock`);
8142
+ }
8143
+ function acquireRunnerLock(options) {
8144
+ const path = runnerLockPath(options.machineId, options.configPath);
8145
+ const isPidAlive = options.isPidAlive ?? processIsAlive;
8146
+ const record = {
8147
+ version: 1,
8148
+ machineId: options.machineId,
8149
+ runnerId: options.runnerId,
8150
+ pid: options.pid ?? process.pid,
8151
+ cwd: options.cwd,
8152
+ workspace: options.workspace,
8153
+ startedAt: (options.now ?? (() => new Date))().toISOString(),
8154
+ cliVersion: options.cliVersion ?? linzumiCliVersion
8155
+ };
8156
+ writeLockOrHandleExisting(path, record, isPidAlive, options.beforeReadExistingLock, options.beforeReplaceStaleLock);
8157
+ return {
8158
+ path,
8159
+ record,
8160
+ release: () => releaseRunnerLock(path, record)
8161
+ };
8162
+ }
8163
+ function writeLockOrHandleExisting(path, record, isPidAlive, beforeReadExistingLock, beforeReplaceStaleLock) {
8164
+ if (tryCreateLock(path, record)) {
8165
+ return;
8166
+ }
8167
+ beforeReadExistingLock?.();
8168
+ const existing = readRunnerLockIfPresent(path);
8169
+ if (existing === undefined) {
8170
+ if (tryCreateLock(path, record)) {
8171
+ return;
8172
+ }
8173
+ throw new Error(`another Linzumi runner lock appeared while starting: ${path}`);
8174
+ }
8175
+ if (isPidAlive(existing.pid)) {
8176
+ throw new Error(activeRunnerLockMessage(path, existing));
8177
+ }
8178
+ beforeReplaceStaleLock?.();
8179
+ withStaleReplacementLock(path, isPidAlive, () => {
8180
+ const latest = existsSync5(path) ? readRunnerLock(path) : undefined;
8181
+ if (latest !== undefined && isPidAlive(latest.pid)) {
8182
+ throw new Error(activeRunnerLockMessage(path, latest));
8183
+ }
8184
+ if (latest !== undefined) {
8185
+ unlinkSync2(path);
8186
+ }
8187
+ if (!tryCreateLock(path, record)) {
8188
+ throw new Error(`another Linzumi runner lock appeared while starting: ${path}`);
8189
+ }
8190
+ });
8191
+ }
8192
+ function tryCreateLock(path, record) {
8193
+ mkdirSync6(dirname5(path), { recursive: true });
8194
+ try {
8195
+ const fd = openSync2(path, "wx");
8196
+ try {
8197
+ writeSync(fd, `${JSON.stringify(record, null, 2)}
8198
+ `);
8199
+ } finally {
8200
+ closeSync(fd);
8201
+ }
8202
+ return true;
8203
+ } catch (error) {
8204
+ if (isNodeErrorCode2(error, "EEXIST")) {
8205
+ return false;
8206
+ }
8207
+ throw error;
8208
+ }
8209
+ }
8210
+ function withStaleReplacementLock(path, isPidAlive, callback) {
8211
+ const replacementPath = `${path}.replace`;
8212
+ while (true) {
8213
+ try {
8214
+ const fd = openSync2(replacementPath, "wx");
8215
+ try {
8216
+ writeSync(fd, `${process.pid}
8217
+ `);
8218
+ callback();
8219
+ } finally {
8220
+ closeSync(fd);
8221
+ unlinkSync2(replacementPath);
8222
+ }
8223
+ return;
8224
+ } catch (error) {
8225
+ if (isNodeErrorCode2(error, "EEXIST")) {
8226
+ const replacementPid = readReplacementLockPidIfPresent(replacementPath);
8227
+ if (replacementPid === undefined) {
8228
+ continue;
8229
+ }
8230
+ if (isPidAlive(replacementPid)) {
8231
+ throw new Error([
8232
+ "another Linzumi runner is already replacing a stale runner lock",
8233
+ `lock: ${path}`,
8234
+ "Wait for that startup to finish, then retry."
8235
+ ].join(`
8236
+ `));
8237
+ }
8238
+ unlinkSync2(replacementPath);
8239
+ continue;
8240
+ }
8241
+ throw error;
8242
+ }
8243
+ }
8244
+ }
8245
+ function readReplacementLockPidIfPresent(path) {
8246
+ let value;
8247
+ try {
8248
+ value = readFileSync4(path, "utf8").trim();
8249
+ } catch (error) {
8250
+ if (isNodeErrorCode2(error, "ENOENT")) {
8251
+ return;
8252
+ }
8253
+ throw error;
8254
+ }
8255
+ const pid = Number.parseInt(value, 10);
8256
+ if (pid.toString() !== value || pid <= 0) {
8257
+ throw new Error(`invalid Linzumi runner replacement lock: ${path}`);
8258
+ }
8259
+ return pid;
8260
+ }
8261
+ function releaseRunnerLock(path, record) {
8262
+ const current = readRunnerLockForRelease(path);
8263
+ if (current !== undefined && current.machineId === record.machineId && current.runnerId === record.runnerId && current.pid === record.pid) {
8264
+ unlinkSync2(path);
8265
+ }
8266
+ }
8267
+ function readRunnerLockForRelease(path) {
8268
+ try {
8269
+ return readRunnerLockIfPresent(path);
8270
+ } catch (_error) {
8271
+ return;
8272
+ }
8273
+ }
8274
+ function readRunnerLockIfPresent(path) {
8275
+ if (!existsSync5(path)) {
8276
+ return;
8277
+ }
8278
+ try {
8279
+ return readRunnerLock(path);
8280
+ } catch (error) {
8281
+ if (isNodeErrorCode2(error, "ENOENT")) {
8282
+ return;
8283
+ }
8284
+ throw error;
8285
+ }
8286
+ }
8287
+ function readRunnerLock(path) {
8288
+ const parsed = JSON.parse(readFileSync4(path, "utf8"));
8289
+ if (!isRunnerLockRecord(parsed)) {
8290
+ throw new Error(`invalid Linzumi runner lock: ${path}`);
8291
+ }
8292
+ return parsed;
8293
+ }
8294
+ function isRunnerLockRecord(value) {
8295
+ 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() !== "";
8296
+ }
8297
+ function workspaceValid(value) {
8298
+ return value === null || typeof value === "string" && value.trim() !== "";
8299
+ }
8300
+ function processIsAlive(pid) {
8301
+ try {
8302
+ process.kill(pid, 0);
8303
+ return true;
8304
+ } catch (error) {
8305
+ return isNodeErrorCode2(error, "ESRCH") ? false : true;
8306
+ }
8307
+ }
8308
+ function activeRunnerLockMessage(path, record) {
8309
+ const workspace = record.workspace === null ? "workspace: unknown" : `workspace: ${record.workspace}`;
8310
+ return [
8311
+ "another Linzumi runner is already running for this machine",
8312
+ `runner id: ${record.runnerId}`,
8313
+ `pid: ${record.pid}`,
8314
+ `cwd: ${record.cwd}`,
8315
+ workspace,
8316
+ `CLI version: ${record.cliVersion}`,
8317
+ `started at: ${record.startedAt}`,
8318
+ `lock: ${path}`,
8319
+ "Stop that process first, then retry. If the process has already exited, remove the stale lock file and retry."
8320
+ ].join(`
8321
+ `);
8322
+ }
8323
+ function isNodeErrorCode2(error, code) {
8324
+ return typeof error === "object" && error !== null && "code" in error && error.code === code;
8325
+ }
8326
+
7950
8327
  // src/runnerConsoleReporter.ts
7951
8328
  function reportRunnerConsoleEvent(event, payload) {
7952
8329
  const line = formatRunnerConsoleEvent(event, payload);
@@ -7958,7 +8335,15 @@ function reportRunnerConsoleEvent(event, payload) {
7958
8335
  function formatRunnerConsoleEvent(event, payload) {
7959
8336
  switch (event) {
7960
8337
  case "runner.instance_started":
7961
- return `Runner connected: instance=${text(payload.instanceId)} codex=${text(payload.codexUrl)}`;
8338
+ return connectedRunnerMessage(payload);
8339
+ case "runner.replaced":
8340
+ return [
8341
+ "Runner replaced: another Linzumi CLI connected from this machine.",
8342
+ `New runner: ${text(payload.replacementRunnerId)}`,
8343
+ `Version: ${text(payload.replacementVersion)}`,
8344
+ "This process is exiting."
8345
+ ].join(`
8346
+ `);
7962
8347
  case "kandan.message_ignored":
7963
8348
  return `Incoming message from ${sender(payload)}: ignored for reason ${text(payload.reason)}`;
7964
8349
  case "kandan.message_queued":
@@ -7993,6 +8378,40 @@ function formatRunnerConsoleEvent(event, payload) {
7993
8378
  return;
7994
8379
  }
7995
8380
  }
8381
+ function connectedRunnerMessage(payload) {
8382
+ return [
8383
+ "Connected to Linzumi",
8384
+ optionalLine("Computer", payload.hostname),
8385
+ optionalLine("Workspace", payload.workspace),
8386
+ `Runner: ${text(payload.runnerId)}`,
8387
+ `CLI: ${text(payload.version)}`,
8388
+ optionalLine("Codex", payload.codexUrl),
8389
+ ...replacementLines(payload.replacedRunners)
8390
+ ].filter((line) => line !== undefined).join(`
8391
+ `);
8392
+ }
8393
+ function optionalLine(label, value) {
8394
+ const normalized = stringValue2(value) ?? numberValue(value)?.toString();
8395
+ return normalized === undefined ? undefined : `${label}: ${normalized}`;
8396
+ }
8397
+ function replacementLines(value) {
8398
+ if (!Array.isArray(value)) {
8399
+ return [];
8400
+ }
8401
+ return value.flatMap((entry) => {
8402
+ if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
8403
+ return [];
8404
+ }
8405
+ const record = entry;
8406
+ const runnerId = stringValue2(record.runnerId);
8407
+ if (runnerId === undefined) {
8408
+ return [];
8409
+ }
8410
+ const version = stringValue2(record.version);
8411
+ const suffix = version === undefined ? "" : ` (CLI ${version})`;
8412
+ return [`Replaced older runner from this machine: ${runnerId}${suffix}`];
8413
+ });
8414
+ }
7996
8415
  function sender(payload) {
7997
8416
  const slug = stringValue2(payload.actor_slug);
7998
8417
  const userId = numberValue(payload.actor_user_id);
@@ -8056,6 +8475,21 @@ async function runLocalCodexRunner(options) {
8056
8475
  kandanUrl: options.kandanUrl
8057
8476
  });
8058
8477
  try {
8478
+ if (options.machineId !== undefined) {
8479
+ const runnerLock = acquireRunnerLock({
8480
+ machineId: options.machineId,
8481
+ runnerId: options.runnerId,
8482
+ cwd: options.cwd,
8483
+ workspace: runnerWorkspaceSlug(options) ?? null,
8484
+ configPath: options.runnerLockConfigPath
8485
+ });
8486
+ cleanup.actions.push(() => runnerLock.release());
8487
+ log("runner.lock_acquired", {
8488
+ path: runnerLock.path,
8489
+ machineId: options.machineId,
8490
+ runnerId: options.runnerId
8491
+ });
8492
+ }
8059
8493
  return await openLocalCodexRunner(options, log, cleanup, close);
8060
8494
  } catch (error) {
8061
8495
  await close().catch(() => {
@@ -8123,9 +8557,11 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
8123
8557
  const kandan = await connectPhoenixClient(options.kandanUrl, options.token, options.socketFactory);
8124
8558
  cleanup.actions.push(() => kandan.close());
8125
8559
  const topic = `local_runner:${options.runnerId}`;
8560
+ const clientId = options.machineId ?? options.runnerId;
8126
8561
  const joinPayload = () => ({
8127
8562
  clientName: "kandan-local-codex-runner",
8128
- version: "0.0.1",
8563
+ clientId,
8564
+ version: linzumiCliVersion,
8129
8565
  workspace: runnerWorkspaceSlug(options) ?? null,
8130
8566
  channel: options.channelSession?.channelSlug ?? null,
8131
8567
  capabilities: capabilitiesPayload()
@@ -8140,7 +8576,10 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
8140
8576
  }
8141
8577
  dispatcher(control);
8142
8578
  });
8143
- await kandan.join(topic, joinPayload(), { rejoinPayload: joinPayload });
8579
+ const joinResponse = await kandan.join(topic, joinPayload(), {
8580
+ rejoinPayload: joinPayload
8581
+ });
8582
+ const replacedRunners = replacementRunnerSummaries(objectValue(joinResponse)?.replaced_runners);
8144
8583
  const started = options.codexUrl === undefined ? await startOwnedCodexAppServer(options) : undefined;
8145
8584
  if (started !== undefined) {
8146
8585
  cleanup.actions.push(() => {
@@ -8151,7 +8590,7 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
8151
8590
  if (codexUrl === undefined) {
8152
8591
  throw new Error("missing codex app-server websocket URL");
8153
8592
  }
8154
- const instanceId = `codex-${randomUUID2()}`;
8593
+ const instanceId = `codex-${randomUUID3()}`;
8155
8594
  const publishLocalEditorStatus = (payload) => {
8156
8595
  kandan.push(topic, "local_editor_status", payload).catch((error) => {
8157
8596
  log("kandan.local_editor_status_push_failed", {
@@ -8199,6 +8638,7 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
8199
8638
  const runtimeDefaults = runnerRuntimeDefaults(options);
8200
8639
  const instancePayload = {
8201
8640
  instanceId,
8641
+ clientId,
8202
8642
  codexUrl,
8203
8643
  tuiLaunched: options.launchTui,
8204
8644
  cwd: options.cwd,
@@ -8211,7 +8651,15 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
8211
8651
  fast: options.fast ?? false
8212
8652
  };
8213
8653
  await kandan.push(topic, "instance_started", instancePayload);
8214
- log("runner.instance_started", { instanceId, codexUrl });
8654
+ log("runner.instance_started", {
8655
+ runnerId: options.runnerId,
8656
+ hostname: runnerHost,
8657
+ workspace: runnerWorkspaceSlug(options) ?? null,
8658
+ version: linzumiCliVersion,
8659
+ instanceId,
8660
+ codexUrl,
8661
+ replacedRunners
8662
+ });
8215
8663
  const channelSession = options.channelSession === undefined ? undefined : await attachChannelSession({
8216
8664
  kandan,
8217
8665
  codex,
@@ -8312,6 +8760,7 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
8312
8760
  };
8313
8761
  const heartbeatPayload = () => ({
8314
8762
  instanceId,
8763
+ clientId,
8315
8764
  codexUrl,
8316
8765
  cwd: options.cwd,
8317
8766
  hostname: runnerHost,
@@ -8390,6 +8839,20 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
8390
8839
  });
8391
8840
  const handleControl = (control) => {
8392
8841
  log("kandan.control", { control });
8842
+ if (control.type === "replace_runner") {
8843
+ log("runner.replaced", {
8844
+ runnerId: options.runnerId,
8845
+ reason: control.reason,
8846
+ replacementRunnerId: control.replacementRunnerId,
8847
+ replacementVersion: control.replacementVersion
8848
+ });
8849
+ close().catch((error) => {
8850
+ log("runner.replace_close_failed", {
8851
+ message: error instanceof Error ? error.message : String(error)
8852
+ });
8853
+ });
8854
+ return;
8855
+ }
8393
8856
  if (!controlTargetsInstance(control, instanceId)) {
8394
8857
  log("kandan.control_ignored", {
8395
8858
  reason: "instance_id_mismatch",
@@ -8604,8 +9067,24 @@ function normalizedWorkDescription(value) {
8604
9067
  const normalized = value?.trim();
8605
9068
  return normalized === undefined || normalized === "" ? undefined : normalized;
8606
9069
  }
9070
+ function replacementRunnerSummaries(value) {
9071
+ const entries = arrayValue(value) ?? [];
9072
+ return entries.flatMap((entry) => {
9073
+ const record = objectValue(entry);
9074
+ const runnerId = stringValue(record?.runnerId);
9075
+ if (runnerId === undefined) {
9076
+ return [];
9077
+ }
9078
+ return [
9079
+ {
9080
+ runnerId,
9081
+ version: stringValue(record?.version) ?? null
9082
+ }
9083
+ ];
9084
+ });
9085
+ }
8607
9086
  function makeRunnerLogger(options) {
8608
- return createRunnerLogger(options.logFile ?? join6(options.cwd, ".linzumi-runner.log"), options.launchTui ? undefined : reportRunnerConsoleEvent);
9087
+ return createRunnerLogger(options.logFile ?? join8(options.cwd, ".linzumi-runner.log"), options.launchTui ? undefined : reportRunnerConsoleEvent);
8609
9088
  }
8610
9089
  function installCleanupHandlers(close) {
8611
9090
  const closeAndExit = () => {
@@ -9054,11 +9533,12 @@ function optionalThreadControlField(control, field) {
9054
9533
  }
9055
9534
  function startInstanceRuntimeSettings(options, control) {
9056
9535
  const defaults = runnerRuntimeDefaults(options);
9536
+ const sandbox = control.sandbox ?? defaults.sandbox;
9057
9537
  return {
9058
9538
  model: control.model ?? defaults.model,
9059
9539
  reasoningEffort: control.reasoningEffort ?? defaults.reasoningEffort,
9060
- approvalPolicy: control.approvalPolicy ?? defaults.approvalPolicy,
9061
- sandbox: control.sandbox ?? defaults.sandbox,
9540
+ approvalPolicy: codexApprovalPolicyForRequest(control.approvalPolicy ?? defaults.approvalPolicy, sandbox),
9541
+ sandbox,
9062
9542
  fast: control.fast ?? options.fast
9063
9543
  };
9064
9544
  }
@@ -9077,11 +9557,12 @@ function runnerWorkspaceSlug(options) {
9077
9557
  function runnerRuntimeDefaults(options) {
9078
9558
  const session = options.channelSession;
9079
9559
  const defaults = options.runtimeDefaults;
9560
+ const sandbox = defaults?.sandbox ?? session?.sandbox;
9080
9561
  return {
9081
9562
  model: defaults?.model ?? session?.model,
9082
9563
  reasoningEffort: defaults?.reasoningEffort ?? session?.reasoningEffort,
9083
- approvalPolicy: defaults?.approvalPolicy ?? session?.approvalPolicy,
9084
- sandbox: defaults?.sandbox ?? session?.sandbox
9564
+ approvalPolicy: codexApprovalPolicySetting(defaults?.approvalPolicy ?? session?.approvalPolicy, sandbox),
9565
+ sandbox
9085
9566
  };
9086
9567
  }
9087
9568
  function isUpdateRunnerConfigControl(control) {
@@ -9100,9 +9581,9 @@ function configuredAllowedCwds(values) {
9100
9581
  const allowedCwds = [];
9101
9582
  const missingAllowedCwds = [];
9102
9583
  for (const value of normalizeAllowedCwds(values)) {
9103
- const absolutePath = resolve5(expandUserPath(value));
9584
+ const absolutePath = resolve6(expandUserPath(value));
9104
9585
  try {
9105
- const realPath = realpathSync4(absolutePath);
9586
+ const realPath = realpathSync5(absolutePath);
9106
9587
  allowedCwds.push(...realPath === absolutePath ? [realPath] : [realPath, absolutePath]);
9107
9588
  } catch (error) {
9108
9589
  if (isMissingAllowedCwdError(error)) {
@@ -9125,18 +9606,18 @@ function allowedCwdSuggestions(cwd, allowedCwds) {
9125
9606
  }
9126
9607
 
9127
9608
  // 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";
9609
+ import { existsSync as existsSync6, mkdirSync as mkdirSync7, readFileSync as readFileSync5, writeFileSync as writeFileSync5 } from "node:fs";
9610
+ import { homedir as homedir6 } from "node:os";
9611
+ import { dirname as dirname6, join as join9 } from "node:path";
9131
9612
  function defaultAuthFilePath() {
9132
- const base = process.env.KANDAN_HOME ?? join7(homedir5(), ".kandan");
9133
- return join7(base, "auth.json");
9613
+ const base = process.env.KANDAN_HOME ?? join9(homedir6(), ".kandan");
9614
+ return join9(base, "auth.json");
9134
9615
  }
9135
9616
  function readCachedLocalRunnerToken(kandanUrl, authFilePath = defaultAuthFilePath()) {
9136
- if (!existsSync4(authFilePath)) {
9617
+ if (!existsSync6(authFilePath)) {
9137
9618
  return;
9138
9619
  }
9139
- const authFile = parseAuthFile(readFileSync3(authFilePath, "utf8"));
9620
+ const authFile = parseAuthFile(readFileSync5(authFilePath, "utf8"));
9140
9621
  const kandanBaseUrl = kandanHttpBaseUrl(kandanUrl);
9141
9622
  const entry = authFile.local_codex_runner?.[kandanBaseUrl];
9142
9623
  if (entry === undefined || entry.access_token.trim() === "") {
@@ -9154,7 +9635,7 @@ function readCachedLocalRunnerToken(kandanUrl, authFilePath = defaultAuthFilePat
9154
9635
  }
9155
9636
  function writeCachedLocalRunnerToken(args) {
9156
9637
  const authFilePath = args.authFilePath ?? defaultAuthFilePath();
9157
- const existing = existsSync4(authFilePath) ? parseAuthFile(readFileSync3(authFilePath, "utf8")) : { version: 1 };
9638
+ const existing = existsSync6(authFilePath) ? parseAuthFile(readFileSync5(authFilePath, "utf8")) : { version: 1 };
9158
9639
  const kandanBaseUrl = kandanHttpBaseUrl(args.kandanUrl);
9159
9640
  const issuedAt = new Date;
9160
9641
  const expiresAt = args.expiresInSeconds === undefined ? undefined : new Date(issuedAt.getTime() + args.expiresInSeconds * 1000).toISOString();
@@ -9170,8 +9651,8 @@ function writeCachedLocalRunnerToken(args) {
9170
9651
  }
9171
9652
  }
9172
9653
  };
9173
- mkdirSync5(dirname4(authFilePath), { recursive: true });
9174
- writeFileSync4(authFilePath, `${JSON.stringify(next, null, 2)}
9654
+ mkdirSync7(dirname6(authFilePath), { recursive: true });
9655
+ writeFileSync5(authFilePath, `${JSON.stringify(next, null, 2)}
9175
9656
  `, "utf8");
9176
9657
  return {
9177
9658
  accessToken: args.accessToken,
@@ -9263,102 +9744,12 @@ async function acquireAndCacheToken(args) {
9263
9744
  return token.accessToken;
9264
9745
  }
9265
9746
 
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
9747
  // src/defaultUrls.ts
9357
9748
  var defaultLinzumiHttpUrl = "https://serve.linzumi.com";
9358
9749
  var defaultLinzumiWebSocketUrl = "wss://serve.linzumi.com";
9359
9750
 
9360
9751
  // src/kandanTls.ts
9361
- import { existsSync as existsSync6, readFileSync as readFileSync5 } from "node:fs";
9752
+ import { existsSync as existsSync7, readFileSync as readFileSync6 } from "node:fs";
9362
9753
  import { Agent } from "undici";
9363
9754
  import { WebSocket as WsWebSocket } from "ws";
9364
9755
  function kandanTlsTrustFromEnv() {
@@ -9369,10 +9760,10 @@ function kandanTlsTrustFromCaFile(caFile) {
9369
9760
  return;
9370
9761
  }
9371
9762
  const trimmed = caFile.trim();
9372
- if (!existsSync6(trimmed)) {
9763
+ if (!existsSync7(trimmed)) {
9373
9764
  throw new Error(`KANDAN_TLS_CA_FILE does not exist: ${trimmed}`);
9374
9765
  }
9375
- const ca = readFileSync5(trimmed, "utf8");
9766
+ const ca = readFileSync6(trimmed, "utf8");
9376
9767
  return {
9377
9768
  caFile: trimmed,
9378
9769
  ca,
@@ -9401,8 +9792,8 @@ function trustedWebSocketFactory(trust, WebSocketImpl = WsWebSocket) {
9401
9792
  }
9402
9793
 
9403
9794
  // 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";
9795
+ import { existsSync as existsSync8, mkdirSync as mkdirSync8, readFileSync as readFileSync7, writeFileSync as writeFileSync6 } from "node:fs";
9796
+ import { dirname as dirname7, join as join10 } from "node:path";
9406
9797
  import { homedir as homedir7 } from "node:os";
9407
9798
  async function runAgentCliCommand(args, deps = {
9408
9799
  fetchImpl: fetch,
@@ -10002,7 +10393,7 @@ function agentTokenFile(flags) {
10002
10393
  return flags.get("agent-token-file") ?? defaultAgentTokenFilePath();
10003
10394
  }
10004
10395
  function defaultAgentTokenFilePath() {
10005
- return join8(homedir7(), ".linzumi", "agent-token.json");
10396
+ return join10(homedir7(), ".linzumi", "agent-token.json");
10006
10397
  }
10007
10398
  function normalizedApiUrl(apiUrl) {
10008
10399
  return apiUrl.endsWith("/") ? apiUrl : `${apiUrl}/`;
@@ -10011,10 +10402,10 @@ function authorizationHeaders(token) {
10011
10402
  return { authorization: `Bearer ${token}` };
10012
10403
  }
10013
10404
  function readOptionalTextFile(path) {
10014
- return existsSync7(path) ? readFileSync6(path, "utf8") : undefined;
10405
+ return existsSync8(path) ? readFileSync7(path, "utf8") : undefined;
10015
10406
  }
10016
10407
  function writeTextFile(path, content) {
10017
- mkdirSync7(dirname6(path), { recursive: true });
10408
+ mkdirSync8(dirname7(path), { recursive: true });
10018
10409
  writeFileSync6(path, content);
10019
10410
  }
10020
10411
  function readStoredAgentTokenFile(path, readTextFile = readOptionalTextFile) {
@@ -10091,8 +10482,8 @@ Launch target:
10091
10482
  }
10092
10483
 
10093
10484
  // 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";
10485
+ import { existsSync as existsSync9, mkdirSync as mkdirSync9, readFileSync as readFileSync8, rmSync as rmSync2, writeFileSync as writeFileSync7 } from "node:fs";
10486
+ import { dirname as dirname8, join as join11, resolve as resolve7 } from "node:path";
10096
10487
  import { fileURLToPath } from "node:url";
10097
10488
  var defaultHelloLinzumiProjectDir = "/tmp/hello_linzumi";
10098
10489
  var defaultHelloLinzumiProjectName = "hello_linzumi";
@@ -10100,8 +10491,8 @@ var defaultHelloLinzumiParentDir = "/tmp";
10100
10491
  var defaultHelloLinzumiPort = 8787;
10101
10492
  var defaultHelloLinzumiHost = "0.0.0.0";
10102
10493
  var markerFile = ".linzumi-demo-project";
10103
- var moduleDir = dirname7(fileURLToPath(import.meta.url));
10104
- var linzumiLogoSvg = readFileSync7(join9(moduleDir, "assets", "linzumi-logo.svg"), "utf8");
10494
+ var moduleDir = dirname8(fileURLToPath(import.meta.url));
10495
+ var linzumiLogoSvg = readFileSync8(join11(moduleDir, "assets", "linzumi-logo.svg"), "utf8");
10105
10496
  function createHelloLinzumiProject(input = {}) {
10106
10497
  const options = typeof input === "string" ? { rootPath: input } : input;
10107
10498
  const root = resolveHelloProjectRoot(options);
@@ -10109,9 +10500,9 @@ function createHelloLinzumiProject(input = {}) {
10109
10500
  const host = normalizeHost(options.host);
10110
10501
  assertTcpPort(port);
10111
10502
  assertWritableDemoRoot(root, options.reset === true);
10112
- mkdirSync8(join9(root, "src"), { recursive: true });
10503
+ mkdirSync9(join11(root, "src"), { recursive: true });
10113
10504
  for (const file of demoFiles({ root, port, host })) {
10114
- writeFileSync7(join9(root, file.path), file.content, "utf8");
10505
+ writeFileSync7(join11(root, file.path), file.content, "utf8");
10115
10506
  }
10116
10507
  return {
10117
10508
  root,
@@ -10152,11 +10543,11 @@ function assertTcpPort(port) {
10152
10543
  throw new Error("--port must be a TCP port from 1 to 65535");
10153
10544
  }
10154
10545
  function assertWritableDemoRoot(root, reset) {
10155
- if (!existsSync8(root)) {
10546
+ if (!existsSync9(root)) {
10156
10547
  return;
10157
10548
  }
10158
- const markerPath = join9(root, markerFile);
10159
- const isDemoRoot = existsSync8(markerPath) && readFileSync7(markerPath, "utf8").trim() === "hello-linzumi";
10549
+ const markerPath = join11(root, markerFile);
10550
+ const isDemoRoot = existsSync9(markerPath) && readFileSync8(markerPath, "utf8").trim() === "hello-linzumi";
10160
10551
  if (isDemoRoot && reset) {
10161
10552
  rmSync2(root, { recursive: true, force: true });
10162
10553
  return;
@@ -10655,27 +11046,30 @@ To kick the agent off:
10655
11046
 
10656
11047
  // src/commanderDaemon.ts
10657
11048
  import {
10658
- existsSync as existsSync9,
10659
- closeSync,
10660
- mkdirSync as mkdirSync9,
10661
- openSync as openSync2,
10662
- readFileSync as readFileSync8,
11049
+ existsSync as existsSync10,
11050
+ closeSync as closeSync2,
11051
+ mkdirSync as mkdirSync10,
11052
+ openSync as openSync3,
11053
+ readFileSync as readFileSync9,
10663
11054
  watch,
10664
11055
  writeFileSync as writeFileSync8
10665
11056
  } from "node:fs";
10666
11057
  import { homedir as homedir8 } from "node:os";
10667
- import { dirname as dirname8, join as join10, resolve as resolve8 } from "node:path";
11058
+ import { dirname as dirname9, join as join12, resolve as resolve8 } from "node:path";
10668
11059
  import { execFileSync, spawn as spawn7 } from "node:child_process";
10669
11060
  import { fileURLToPath as fileURLToPath2 } from "node:url";
10670
- var connectedMarker = "Runner connected:";
11061
+ var connectedMarkers = ["Connected to Linzumi", "Runner connected:"];
10671
11062
  function commanderStatusDir() {
10672
- return join10(homedir8(), ".linzumi", "commanders");
11063
+ return join12(homedir8(), ".linzumi", "commanders");
10673
11064
  }
10674
11065
  function commanderStatusFile(runnerId, statusDir = commanderStatusDir()) {
10675
- return join10(statusDir, `${safeRunnerId(runnerId)}.json`);
11066
+ return join12(statusDir, `${safeRunnerId(runnerId)}.json`);
10676
11067
  }
10677
11068
  function defaultCommanderLogFile(runnerId, statusDir = commanderStatusDir()) {
10678
- return join10(statusDir, `${safeRunnerId(runnerId)}.log`);
11069
+ return join12(statusDir, `${safeRunnerId(runnerId)}.log`);
11070
+ }
11071
+ function commanderLogIsConnected(log) {
11072
+ return connectedMarkers.some((marker) => log.includes(marker));
10679
11073
  }
10680
11074
  function startCommanderDaemon(options) {
10681
11075
  const statusDir = options.statusDir ?? commanderStatusDir();
@@ -10693,10 +11087,10 @@ function startCommanderDaemon(options) {
10693
11087
  "--log-file",
10694
11088
  logFile
10695
11089
  ];
10696
- mkdirSync9(statusDir, { recursive: true });
10697
- mkdirSync9(dirname8(logFile), { recursive: true });
10698
- const out = openSync2(logFile, "a");
10699
- const err = openSync2(logFile, "a");
11090
+ mkdirSync10(statusDir, { recursive: true });
11091
+ mkdirSync10(dirname9(logFile), { recursive: true });
11092
+ const out = openSync3(logFile, "a");
11093
+ const err = openSync3(logFile, "a");
10700
11094
  writeCliAuditEvent("process.spawn", {
10701
11095
  command: nodeBin,
10702
11096
  args: command,
@@ -10715,8 +11109,8 @@ function startCommanderDaemon(options) {
10715
11109
  pid: child.pid,
10716
11110
  purpose: "commander_daemon.start"
10717
11111
  }, { sessionId: options.runnerId });
10718
- closeSync(out);
10719
- closeSync(err);
11112
+ closeSync2(out);
11113
+ closeSync2(err);
10720
11114
  child.unref();
10721
11115
  if (child.pid === undefined) {
10722
11116
  throw new Error("commander daemon did not report a pid");
@@ -10738,15 +11132,15 @@ function startCommanderDaemon(options) {
10738
11132
  }
10739
11133
  function commanderDaemonStatus(runnerId, statusDir = commanderStatusDir(), processIdentityReader = readProcessIdentity) {
10740
11134
  const statusFile = commanderStatusFile(runnerId, statusDir);
10741
- if (!existsSync9(statusFile)) {
11135
+ if (!existsSync10(statusFile)) {
10742
11136
  return { status: "missing", runnerId, statusFile };
10743
11137
  }
10744
- const record = parseRecord(readFileSync8(statusFile, "utf8"));
11138
+ const record = parseRecord(readFileSync9(statusFile, "utf8"));
10745
11139
  return processIsRunning(record.pid) && processMatchesRecord(record, processIdentityReader) ? { status: "running", record } : { status: "stopped", record };
10746
11140
  }
10747
11141
  async function waitForCommanderDaemon(options) {
10748
11142
  const now = options.now ?? (() => Date.now());
10749
- const readTextFile = options.readTextFile ?? ((path) => existsSync9(path) ? readFileSync8(path, "utf8") : undefined);
11143
+ const readTextFile = options.readTextFile ?? ((path) => existsSync10(path) ? readFileSync9(path, "utf8") : undefined);
10750
11144
  const statusImpl = options.statusImpl ?? commanderDaemonStatus;
10751
11145
  const deadline = now() + options.timeoutMs;
10752
11146
  while (now() <= deadline) {
@@ -10761,12 +11155,12 @@ async function waitForCommanderDaemon(options) {
10761
11155
  if (log === undefined) {
10762
11156
  return { ok: false, reason: "timeout" };
10763
11157
  }
10764
- if (log.includes(connectedMarker)) {
11158
+ if (commanderLogIsConnected(log)) {
10765
11159
  return { ok: true, record: status.record };
10766
11160
  }
10767
11161
  await waitForFileChangeOrTimeout(status.record.logFile, deadline, now, () => {
10768
11162
  const updatedLog = readTextFile(status.record.logFile);
10769
- return updatedLog !== undefined && updatedLog.includes(connectedMarker);
11163
+ return updatedLog !== undefined && commanderLogIsConnected(updatedLog);
10770
11164
  });
10771
11165
  }
10772
11166
  }
@@ -10976,7 +11370,7 @@ async function main(args) {
10976
11370
  process.stdout.write(connectGuideText());
10977
11371
  return;
10978
11372
  case "version":
10979
- process.stdout.write(`linzumi 0.0.41-beta
11373
+ process.stdout.write(`${linzumiCliVersionText}
10980
11374
  `);
10981
11375
  return;
10982
11376
  case "auth":
@@ -10996,18 +11390,18 @@ async function main(args) {
10996
11390
  return;
10997
11391
  case "agentRunner": {
10998
11392
  const options = await parseAgentRunnerArgs(parsed.args);
10999
- await runLocalCodexRunner(options);
11393
+ await runLocalCodexRunner(withLocalMachineId(options));
11000
11394
  return;
11001
11395
  }
11002
11396
  case "start": {
11003
11397
  const options = await parseStartRunnerArgs(parsed.args);
11004
11398
  addAllowedCwd(options.cwd);
11005
- await runLocalCodexRunner(options);
11399
+ await runLocalCodexRunner(withLocalMachineId(options));
11006
11400
  return;
11007
11401
  }
11008
11402
  case "run": {
11009
11403
  const options = await parseRunnerArgs(parsed.args);
11010
- await runLocalCodexRunner(options);
11404
+ await runLocalCodexRunner(withLocalMachineId(options));
11011
11405
  return;
11012
11406
  }
11013
11407
  }
@@ -11318,7 +11712,7 @@ async function parseStartRunnerArgs(args, deps = {
11318
11712
  return {
11319
11713
  kandanUrl,
11320
11714
  token: targetToken,
11321
- runnerId: stringValue3(values, "runner-id") ?? `runner-${randomUUID3()}`,
11715
+ runnerId: stringValue3(values, "runner-id") ?? `runner-${randomUUID4()}`,
11322
11716
  workspaceSlug: target.workspaceSlug,
11323
11717
  cwd,
11324
11718
  codexBin,
@@ -11406,7 +11800,7 @@ async function parseAgentRunnerArgs(args, deps = {
11406
11800
  return {
11407
11801
  kandanUrl,
11408
11802
  token: tokenFile.commanderToken,
11409
- runnerId: stringValue3(values, "runner-id") ?? `agent-runner-${randomUUID3()}`,
11803
+ runnerId: stringValue3(values, "runner-id") ?? `agent-runner-${randomUUID4()}`,
11410
11804
  workspaceSlug: tokenFile.workspaceId,
11411
11805
  cwd,
11412
11806
  codexBin,
@@ -11426,7 +11820,7 @@ async function parseAgentRunnerArgs(args, deps = {
11426
11820
  };
11427
11821
  }
11428
11822
  function readAgentTokenTextFile(path) {
11429
- return existsSync10(path) ? readFileSync9(path, "utf8") : undefined;
11823
+ return existsSync11(path) ? readFileSync10(path, "utf8") : undefined;
11430
11824
  }
11431
11825
  function rejectAgentRunnerTargetingFlags(values) {
11432
11826
  const unsupportedFlags = [
@@ -11502,12 +11896,12 @@ async function parseRunnerArgs(args, deps = {
11502
11896
  process.exit(0);
11503
11897
  }
11504
11898
  if (values.get("version") === true) {
11505
- process.stdout.write(`linzumi 0.0.41-beta
11899
+ process.stdout.write(`${linzumiCliVersionText}
11506
11900
  `);
11507
11901
  process.exit(0);
11508
11902
  }
11509
- const channelTarget = parseChannelSessionTarget(values);
11510
- const kandanUrl = required(values, "linzumi-url");
11903
+ const connectTarget = parseConnectTarget(values);
11904
+ const kandanUrl = stringValue3(values, "linzumi-url") ?? defaultLinzumiWebSocketUrl;
11511
11905
  const cwd = stringValue3(values, "cwd") ?? process.cwd();
11512
11906
  const cwdAllowedCwds = assertConfiguredAllowedCwds([cwd]);
11513
11907
  const localConfiguredAllowedCwds = values.has("allowed-cwd") ? { allowedCwds: [], missingAllowedCwds: [] } : readConfiguredAllowedCwdDetails();
@@ -11518,8 +11912,8 @@ async function parseRunnerArgs(args, deps = {
11518
11912
  const token = await deps.resolveToken({
11519
11913
  kandanUrl,
11520
11914
  explicitToken,
11521
- workspaceSlug: channelTarget?.workspaceSlug,
11522
- channelSlug: channelTarget?.channelSlug,
11915
+ workspaceSlug: connectTarget?.workspaceSlug,
11916
+ channelSlug: connectTarget?.channelSlug,
11523
11917
  authFilePath: stringValue3(values, "auth-file"),
11524
11918
  callbackHost: stringValue3(values, "oauth-callback-host"),
11525
11919
  reportRejectedCachedToken: () => {
@@ -11527,7 +11921,7 @@ async function parseRunnerArgs(args, deps = {
11527
11921
  `);
11528
11922
  }
11529
11923
  });
11530
- const channelSession = parseChannelSession(values, token, channelTarget);
11924
+ const channelSession = parseChannelSession(values, token, connectTarget);
11531
11925
  const editorRuntime = await deps.resolveEditorRuntime({
11532
11926
  kandanUrl,
11533
11927
  token,
@@ -11544,7 +11938,7 @@ async function parseRunnerArgs(args, deps = {
11544
11938
  return {
11545
11939
  kandanUrl,
11546
11940
  token,
11547
- runnerId: stringValue3(values, "runner-id") ?? `runner-${randomUUID3()}`,
11941
+ runnerId: stringValue3(values, "runner-id") ?? `runner-${randomUUID4()}`,
11548
11942
  cwd,
11549
11943
  codexBin,
11550
11944
  codexUrl: stringValue3(values, "codex-url"),
@@ -11558,7 +11952,7 @@ async function parseRunnerArgs(args, deps = {
11558
11952
  editorRuntime: editorRuntime.runtime,
11559
11953
  socketFactory: trustedWebSocketFactory(kandanTlsTrustFromEnv()),
11560
11954
  dependencyStatus,
11561
- workspaceSlug: channelSession?.workspaceSlug ?? singleWorkspaceScopeFromAccessToken(token),
11955
+ workspaceSlug: connectTarget?.workspaceSlug ?? singleWorkspaceScopeFromAccessToken(token),
11562
11956
  runtimeDefaults: runnerRuntimeDefaultsFromValues(values),
11563
11957
  channelSession
11564
11958
  };
@@ -11670,7 +12064,7 @@ function resolveUserPath(pathValue) {
11670
12064
  return resolve9(pathValue);
11671
12065
  }
11672
12066
  function parseChannelSession(values, token, target) {
11673
- if (target === undefined) {
12067
+ if (target === undefined || target.channelSlug === undefined) {
11674
12068
  return;
11675
12069
  }
11676
12070
  const listenUser = stringValue3(values, "listen-user") ?? defaultListenUserFromToken(token);
@@ -11686,7 +12080,7 @@ function parseChannelSession(values, token, target) {
11686
12080
  streamFlushMs: positiveIntegerValue(values, "stream-flush-ms")
11687
12081
  };
11688
12082
  }
11689
- function parseChannelSessionTarget(values) {
12083
+ function parseConnectTarget(values) {
11690
12084
  return parseOptionalChannelTarget(values);
11691
12085
  }
11692
12086
  function defaultListenUserFromToken(token) {
@@ -11699,12 +12093,15 @@ function defaultListenUserFromToken(token) {
11699
12093
  function parseOptionalChannelTarget(values) {
11700
12094
  const channel = stringValue3(values, "channel");
11701
12095
  const workspace = stringValue3(values, "workspace");
11702
- if (channel === undefined && workspace === undefined) {
11703
- return;
12096
+ if (channel === undefined) {
12097
+ if (workspace === undefined) {
12098
+ return;
12099
+ }
12100
+ return { workspaceSlug: workspace };
11704
12101
  }
11705
- return channel !== undefined && channel.includes("/") ? parseChannelPath(channel) : {
12102
+ return channel.includes("/") ? parseChannelPath(channel) : {
11706
12103
  workspaceSlug: workspace ?? required(values, "workspace"),
11707
- channelSlug: channel ?? required(values, "channel")
12104
+ channelSlug: channel
11708
12105
  };
11709
12106
  }
11710
12107
  function parseChannelPath(channel) {
@@ -11717,6 +12114,12 @@ function parseChannelPath(channel) {
11717
12114
  channelSlug: channelSlug.trim()
11718
12115
  };
11719
12116
  }
12117
+ function withLocalMachineId(options) {
12118
+ return {
12119
+ ...options,
12120
+ machineId: ensureLocalMachineId()
12121
+ };
12122
+ }
11720
12123
  function required(values, key) {
11721
12124
  const value = stringValue3(values, key);
11722
12125
  if (value === undefined) {
@@ -11773,19 +12176,19 @@ Usage:
11773
12176
  linzumi commander <folder> [options]
11774
12177
  linzumi start <folder> [options]
11775
12178
  linzumi paths list|add|remove [path]
11776
- linzumi connect --linzumi-url <ws-url> --workspace <slug> --channel <slug> [options]
12179
+ linzumi connect --workspace <slug> [--channel <slug>] [options]
11777
12180
  linzumi auth --linzumi-url <ws-url> [--workspace <slug> --channel <slug>]
11778
12181
 
11779
- Required:
12182
+ Connection:
11780
12183
  --linzumi-url <ws-url> Linzumi backend URL, default ${defaultLinzumiWebSocketUrl}
11781
12184
  (deprecated alias: --kandan-url)
11782
12185
  --token <jwt> Optional override token. Otherwise ~/.linzumi/auth.json is validated or OAuth opens.
11783
12186
  --auth-file <path> Auth cache path, default ~/.linzumi/auth.json
11784
12187
  --oauth-callback-host <ip> Callback host reachable by your browser
11785
12188
 
11786
- Channel binding:
12189
+ Workspace and optional channel binding:
11787
12190
  --workspace <slug> Workspace slug
11788
- --channel <slug|w/c> Channel slug, or workspace/channel
12191
+ --channel <slug|w/c> Optional channel slug, or workspace/channel
11789
12192
  --linzumi-thread-id <uuid> Resume an existing Linzumi thread instead of announcing a new root
11790
12193
  (deprecated alias: --kandan-thread-id)
11791
12194
  --listen-user <user|all> User whose replies are accepted, default authenticated user
@@ -11818,8 +12221,9 @@ Examples:
11818
12221
  linzumi commander daemon --runner-id launch-commander
11819
12222
  linzumi start ~/
11820
12223
  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
12224
+ linzumi connect --workspace <your-workspace> --launch-tui
12225
+ linzumi connect --workspace <your-workspace> --model gpt-5 --reasoning-effort low --fast --launch-tui
12226
+ linzumi connect --workspace <your-workspace> --channel <your-channel>
11823
12227
  linzumi auth --workspace <your-workspace> --channel <your-channel>
11824
12228
  linzumi paths add ~/code/my-app
11825
12229
  linzumi paths list
@@ -11829,9 +12233,9 @@ Examples:
11829
12233
  Missing --listen-user and authenticated user is unavailable.
11830
12234
  linzumi connect --token "$TOKEN" --listen-users sean
11831
12235
  Invalid flag: use --listen-user.
11832
- linzumi connect --workspace <your-workspace> --channel <your-channel> --allowed-cwd /does/not/exist
12236
+ linzumi connect --workspace <your-workspace> --allowed-cwd /does/not/exist
11833
12237
  Invalid --allowed-cwd: allowed cwd roots must exist locally.
11834
- linzumi connect --workspace <your-workspace> --channel <your-channel> --forward-port vite
12238
+ linzumi connect --workspace <your-workspace> --forward-port vite
11835
12239
  Invalid --forward-port: value must be a TCP port from 1 to 65535.
11836
12240
  `;
11837
12241
  }
@@ -11989,7 +12393,7 @@ space, persists this folder to your trusted-paths list, and starts this
11989
12393
  computer as a local Codex runner.
11990
12394
 
11991
12395
  Advanced (when you already know your workspace and channel):
11992
- linzumi connect --workspace <your-workspace> --channel <your-channel>
12396
+ linzumi connect --workspace <your-workspace>
11993
12397
 
11994
12398
  For help:
11995
12399
  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.42-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": {