codex-slot 0.1.3 → 0.1.5

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.
package/README.md CHANGED
@@ -37,7 +37,7 @@ codex-slot import current ~
37
37
  ```
38
38
 
39
39
  `import` copies the official login state into `~/.cslot/homes/<name>` instead of referencing the source HOME directly.
40
- `current` is only an example slot name, not a built-in account.
40
+ `current` is only an example slot name, not a built-in account or workspace.
41
41
 
42
42
  2. Check the latest usage:
43
43
 
@@ -70,7 +70,9 @@ codex-slot start
70
70
  codex-slot start --port 4399
71
71
  ```
72
72
 
73
- `start` will automatically write the required provider config into `~/.codex/config.toml`:
73
+ `start` will automatically write the required provider config into `~/.codex/config.toml`.
74
+ It prefers port `4399` by default and will switch to the next available port automatically when `4399` is busy:
75
+ Each start also generates a fresh local `api_key` and syncs it into the managed provider config.
74
76
 
75
77
  ```bash
76
78
  codex-slot start
@@ -127,7 +129,7 @@ Instead it:
127
129
  ```toml
128
130
  [model_providers.cslot]
129
131
  name = "cslot"
130
- base_url = "http://127.0.0.1:4389/v1"
132
+ base_url = "http://127.0.0.1:4399/v1"
131
133
  http_headers = { Authorization = "Bearer <your-local-api-key>" }
132
134
  wire_api = "responses"
133
135
  ```
@@ -138,6 +140,8 @@ Behavior:
138
140
  - On `cslot stop`, the original `model_provider` line and original `[model_providers.cslot]` block are restored from the saved snapshot
139
141
  - Other providers and settings in `config.toml` are left untouched
140
142
  - If you start with `--port`, the port is saved to `~/.cslot/config.yaml`
143
+ - If you start without `--port`, `4399` is preferred first and the next free port is chosen automatically on conflict
144
+ - Every `start` rotates the local `api_key`, and the new value is written to both `~/.cslot/config.yaml` and the managed provider block
141
145
 
142
146
  ## Data Directory
143
147
 
@@ -7,6 +7,7 @@ exports.getRunningPid = getRunningPid;
7
7
  exports.startManagedService = startManagedService;
8
8
  exports.stopManagedService = stopManagedService;
9
9
  const node_fs_1 = __importDefault(require("node:fs"));
10
+ const node_net_1 = __importDefault(require("node:net"));
10
11
  const node_path_1 = __importDefault(require("node:path"));
11
12
  const node_child_process_1 = require("node:child_process");
12
13
  const codex_config_1 = require("../codex-config");
@@ -57,6 +58,59 @@ function resolveServeEntrypoint() {
57
58
  args: [`${serveBasePath}.js`]
58
59
  };
59
60
  }
61
+ /**
62
+ * 检查指定地址与端口当前是否可绑定,用于启动前规避端口冲突。
63
+ *
64
+ * @param host 监听地址。
65
+ * @param port 待检查端口。
66
+ * @returns Promise,可绑定时返回 `true`,被占用或校验失败时返回 `false`。
67
+ * @throws 无显式抛出。
68
+ */
69
+ function isPortAvailable(host, port) {
70
+ return new Promise((resolve) => {
71
+ const server = node_net_1.default.createServer();
72
+ server.once("error", () => {
73
+ resolve(false);
74
+ });
75
+ server.once("listening", () => {
76
+ server.close(() => resolve(true));
77
+ });
78
+ server.listen(port, host);
79
+ });
80
+ }
81
+ /**
82
+ * 为后台服务挑选最终启动端口。
83
+ *
84
+ * 规则:
85
+ * 1. 若用户显式指定 `--port`,则严格使用该端口,冲突时直接报错。
86
+ * 2. 若未显式指定端口,则优先使用 4399。
87
+ * 3. 若默认候选端口冲突,则从候选端口开始向上查找下一个可用端口。
88
+ *
89
+ * @param host 监听地址。
90
+ * @param currentPort 当前配置中的端口。
91
+ * @param portOverride 用户显式指定的端口文本。
92
+ * @returns Promise,成功时返回最终端口与是否发生自动切换。
93
+ * @throws 当显式指定端口冲突或找不到可用端口时抛出异常。
94
+ */
95
+ async function resolveStartPort(host, currentPort, portOverride) {
96
+ if (portOverride) {
97
+ const port = (0, cli_helpers_1.parsePort)(portOverride);
98
+ if (!(await isPortAvailable(host, port))) {
99
+ throw new Error(`端口已被占用: ${port}`);
100
+ }
101
+ return { port, autoSwitched: false };
102
+ }
103
+ const preferredPort = currentPort === 4389 ? 4399 : currentPort;
104
+ for (let candidate = preferredPort; candidate < preferredPort + 50; candidate += 1) {
105
+ if (await isPortAvailable(host, candidate)) {
106
+ return {
107
+ port: candidate,
108
+ autoSwitched: candidate !== preferredPort
109
+ };
110
+ }
111
+ }
112
+ throw new Error(`未找到可用端口,起始端口: ${preferredPort}`);
113
+ }
60
114
  /**
61
115
  * 启动后台服务,并在需要时将端口写回本地配置。
62
116
  *
@@ -64,23 +118,39 @@ function resolveServeEntrypoint() {
64
118
  * @returns 启动结果,包含是否已在运行、最终端口、PID 和日志路径。
65
119
  * @throws 当端口非法、接管配置失败或子进程启动失败时抛出异常。
66
120
  */
67
- function startManagedService(portOverride) {
121
+ async function startManagedService(portOverride) {
68
122
  const config = (0, config_1.loadConfig)();
69
- const port = portOverride ? (0, cli_helpers_1.parsePort)(portOverride) : config.server.port;
70
- if (portOverride) {
123
+ const { port, autoSwitched } = await resolveStartPort(config.server.host, config.server.port, portOverride);
124
+ const hasExplicitPortOverride = typeof portOverride === "string" && portOverride.length > 0;
125
+ const runningPid = getRunningPid();
126
+ if (runningPid && hasExplicitPortOverride && config.server.port !== port) {
71
127
  config.server.port = port;
72
128
  (0, config_1.saveConfig)(config);
73
129
  }
74
- const runningPid = getRunningPid();
75
130
  if (runningPid) {
76
131
  return {
77
132
  alreadyRunning: true,
78
133
  pid: runningPid,
79
134
  port,
80
- logPath: (0, config_1.getServiceLogPath)()
135
+ logPath: (0, config_1.getServiceLogPath)(),
136
+ autoSwitched: false,
137
+ apiKeyRotated: false
81
138
  };
82
139
  }
83
- (0, codex_config_1.applyManagedCodexConfig)();
140
+ if (hasExplicitPortOverride && config.server.port !== port) {
141
+ config.server.port = port;
142
+ (0, config_1.saveConfig)(config);
143
+ }
144
+ // 每次真正启动服务前都轮换一次本地 api_key,并让受管 config.toml 使用同一新值。
145
+ const persistedConfig = (0, config_1.rotateServerApiKey)(config);
146
+ const runtimeConfig = {
147
+ ...persistedConfig,
148
+ server: {
149
+ ...persistedConfig.server,
150
+ port
151
+ }
152
+ };
153
+ (0, codex_config_1.applyManagedCodexConfig)(undefined, { config: runtimeConfig });
84
154
  const logPath = (0, config_1.getServiceLogPath)();
85
155
  const logFd = node_fs_1.default.openSync(logPath, "a");
86
156
  const serveEntrypoint = resolveServeEntrypoint();
@@ -94,7 +164,9 @@ function startManagedService(portOverride) {
94
164
  alreadyRunning: false,
95
165
  pid: child.pid ?? 0,
96
166
  port,
97
- logPath
167
+ logPath,
168
+ autoSwitched,
169
+ apiKeyRotated: true
98
170
  };
99
171
  }
100
172
  /**
package/dist/cli.js CHANGED
@@ -30,7 +30,7 @@ function configureRootProgram(program) {
30
30
  " cslot status --no-interactive",
31
31
  "",
32
32
  `${(0, text_1.bi)("说明", "Notes")}:`,
33
- ` ${(0, text_1.bi)("`import current ~` 里的 current 只是示例槽位名,不是内置账号。", "`current` in `import current ~` is only an example slot name, not a built-in account.")}`
33
+ ` ${(0, text_1.bi)("`import current ~` 里的 current 只是示例槽位名,不是内置账号或工作空间。", "`current` in `import current ~` is only an example slot name, not a built-in account or workspace.")}`
34
34
  ].join("\n"));
35
35
  }
36
36
  /**
@@ -44,19 +44,19 @@ function registerAccountCommands(program) {
44
44
  program
45
45
  .command("add")
46
46
  .description((0, text_1.bi)("登录并新增一个账号或工作空间", "Login and add a managed slot"))
47
- .argument("<name>", (0, text_1.bi)("账号标识(本地槽位名)", "Local slot name"))
47
+ .argument("<name>", (0, text_1.bi)("账号或工作空间标识(本地槽位名)", "Managed slot name"))
48
48
  .action(async (name) => {
49
49
  await (0, account_commands_1.handleAccountLogin)(name);
50
50
  });
51
51
  program
52
52
  .command("del")
53
- .description((0, text_1.bi)("删除一个已录入账号", "Remove a managed slot"))
54
- .argument("[name]", (0, text_1.bi)("账号标识(本地槽位名),留空时列出全部", "Local slot name"))
53
+ .description((0, text_1.bi)("删除一个已录入账号或工作空间", "Remove a managed slot"))
54
+ .argument("[name]", (0, text_1.bi)("账号或工作空间标识(本地槽位名),留空时列出全部", "Managed slot name"))
55
55
  .action(account_commands_1.handleAccountRemoveCommand);
56
56
  program
57
57
  .command("import")
58
58
  .description((0, text_1.bi)("导入当前或指定 HOME 下的官方 codex 登录态", "Import official Codex auth state from the current or specified HOME"))
59
- .argument("<name>", (0, text_1.bi)("账号标识(本地槽位名,例如 work/current)", "Local slot name, for example work/current"))
59
+ .argument("<name>", (0, text_1.bi)("账号或工作空间标识(本地槽位名,例如 work/current)", "Managed slot name, for example work/current"))
60
60
  .argument("[codexHome]", (0, text_1.bi)("已有 HOME 目录,默认当前用户 HOME", "Source HOME, defaults to the current user HOME"))
61
61
  .addHelpText("after", [
62
62
  "",
@@ -66,9 +66,9 @@ function registerAccountCommands(program) {
66
66
  .action(account_commands_1.handleAccountImport);
67
67
  program
68
68
  .command("rename")
69
- .description((0, text_1.bi)("重命名一个已录入账号", "Rename a managed slot"))
70
- .argument("<oldName>", (0, text_1.bi)("原槽位名", "Old slot name"))
71
- .argument("<newName>", (0, text_1.bi)("新槽位名", "New slot name"))
69
+ .description((0, text_1.bi)("重命名一个已录入账号或工作空间", "Rename a managed slot"))
70
+ .argument("<oldName>", (0, text_1.bi)("原账号或工作空间标识", "Old managed slot name"))
71
+ .argument("<newName>", (0, text_1.bi)("新账号或工作空间标识", "New managed slot name"))
72
72
  .action(account_commands_1.handleAccountRename);
73
73
  }
74
74
  /**
@@ -93,7 +93,7 @@ function registerRuntimeCommands(program) {
93
93
  .addHelpText("after", [
94
94
  "",
95
95
  `${(0, text_1.bi)("说明", "Notes")}:`,
96
- ` ${(0, text_1.bi)("start 会自动接管 `~/.codex/config.toml`,并在指定端口时自动写入该端口;stop 会恢复接管前内容。", "`start` will manage `~/.codex/config.toml` automatically, write the specified port when provided, and `stop` will restore the previous content.")}`,
96
+ ` ${(0, text_1.bi)("start 会自动接管 `~/.codex/config.toml`;默认优先使用 4399,冲突时自动顺延;每次启动都会重新生成本地 api_key;指定端口时会写入该端口;stop 会恢复接管前内容。", "`start` will manage `~/.codex/config.toml` automatically; it prefers 4399 by default, switches to the next free port on conflict, generates a fresh local api_key on every start, writes the specified port when provided, and `stop` restores the previous content.")}`,
97
97
  ].join("\n"))
98
98
  .action(async (options) => {
99
99
  await (0, service_control_1.handleStart)(options.port);
@@ -4,10 +4,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.getDefaultCodexConfigPath = getDefaultCodexConfigPath;
7
- exports.generateServerApiKey = generateServerApiKey;
8
7
  exports.applyManagedCodexConfig = applyManagedCodexConfig;
9
8
  exports.deactivateManagedCodexConfig = deactivateManagedCodexConfig;
10
- const node_crypto_1 = __importDefault(require("node:crypto"));
11
9
  const node_fs_1 = __importDefault(require("node:fs"));
12
10
  const node_path_1 = __importDefault(require("node:path"));
13
11
  const config_1 = require("./config");
@@ -25,14 +23,6 @@ const PROVIDER_BLOCK_END_MARKER = "# <<< cslot provider:cslot <<<";
25
23
  function getDefaultCodexConfigPath() {
26
24
  return node_path_1.default.join(process.env.HOME ?? "", ".codex", "config.toml");
27
25
  }
28
- /**
29
- * 生成随机本地 API Key,避免继续使用固定默认值。
30
- *
31
- * @returns 新的 API Key 字符串,仅包含十六进制字符。
32
- */
33
- function generateServerApiKey() {
34
- return `cslot-${node_crypto_1.default.randomBytes(18).toString("hex")}`;
35
- }
36
26
  /**
37
27
  * 原子方式写入目标文件,避免写入过程中留下半截配置。
38
28
  *
@@ -75,15 +65,16 @@ function buildManagedModelProviderBlock(eol) {
75
65
  * @param eol 目标文件当前使用的换行符。
76
66
  * @returns 带标记的 provider 配置块文本。
77
67
  */
78
- function buildManagedProviderBlock(eol) {
79
- const config = (0, config_1.loadConfig)();
68
+ function buildManagedProviderBlock(eol, config) {
80
69
  return [
81
70
  PROVIDER_BLOCK_START_MARKER,
82
71
  "[model_providers.cslot]",
83
72
  'name = "cslot"',
84
73
  `base_url = "http://${config.server.host}:${config.server.port}/v1"`,
85
- `http_headers = { Authorization = "Bearer ${config.server.api_key}" }`,
86
74
  'wire_api = "responses"',
75
+ "",
76
+ "[model_providers.cslot.http_headers]",
77
+ `Authorization = "Bearer ${config.server.api_key}"`,
87
78
  PROVIDER_BLOCK_END_MARKER
88
79
  ].join(eol);
89
80
  }
@@ -283,9 +274,10 @@ function applyManagedCodexConfig(targetPathOrDir, options) {
283
274
  original_model_provider_block: originalModelProviderLine?.value ?? null,
284
275
  original_cslot_provider_block: originalProviderSection?.value ?? null
285
276
  };
277
+ const config = options?.config ?? (0, config_1.loadConfig)();
286
278
  let nextContent = baseContent;
287
279
  const managedModelProviderBlock = buildManagedModelProviderBlock(eol);
288
- const managedProviderBlock = buildManagedProviderBlock(eol);
280
+ const managedProviderBlock = buildManagedProviderBlock(eol, config);
289
281
  // 先处理 provider 表块,再处理 model_provider 行,避免前面的插入导致后续偏移失效。
290
282
  if (originalProviderSection) {
291
283
  nextContent =
@@ -321,7 +313,6 @@ function applyManagedCodexConfig(targetPathOrDir, options) {
321
313
  writeFileAtomic(targetFile, nextContent);
322
314
  (0, state_1.setManagedCodexConfigState)(snapshot);
323
315
  if (!options?.silent) {
324
- const config = (0, config_1.loadConfig)();
325
316
  console.log((0, text_1.bi)(`已写入: ${targetFile}`, `Written to: ${targetFile}`));
326
317
  console.log(`base_url=http://${config.server.host}:${config.server.port}/v1`);
327
318
  console.log(`api_key=${config.server.api_key}`);
@@ -349,6 +340,5 @@ function deactivateManagedCodexConfig() {
349
340
  const restored = restoreManagedContent(current, managedState);
350
341
  writeFileAtomic(targetFile, restored);
351
342
  (0, state_1.clearManagedCodexConfigState)();
352
- console.log((0, text_1.bi)(`已恢复: ${targetFile}`, `Restored: ${targetFile}`));
353
343
  return targetFile;
354
344
  }
package/dist/config.js CHANGED
@@ -3,6 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.generateServerApiKey = generateServerApiKey;
6
7
  exports.getCslotHome = getCslotHome;
7
8
  exports.getConfigPath = getConfigPath;
8
9
  exports.getPidPath = getPidPath;
@@ -10,10 +11,11 @@ exports.getServiceLogPath = getServiceLogPath;
10
11
  exports.expandHome = expandHome;
11
12
  exports.loadConfig = loadConfig;
12
13
  exports.saveConfig = saveConfig;
14
+ exports.rotateServerApiKey = rotateServerApiKey;
13
15
  exports.getManagedHome = getManagedHome;
14
16
  exports.upsertAccount = upsertAccount;
15
- const node_fs_1 = __importDefault(require("node:fs"));
16
17
  const node_crypto_1 = __importDefault(require("node:crypto"));
18
+ const node_fs_1 = __importDefault(require("node:fs"));
17
19
  const node_os_1 = __importDefault(require("node:os"));
18
20
  const node_path_1 = __importDefault(require("node:path"));
19
21
  const yaml_1 = __importDefault(require("yaml"));
@@ -31,13 +33,13 @@ const configSchema = zod_1.z.object({
31
33
  server: zod_1.z
32
34
  .object({
33
35
  host: zod_1.z.string().default("127.0.0.1"),
34
- port: zod_1.z.number().int().default(4389),
36
+ port: zod_1.z.number().int().default(4399),
35
37
  api_key: zod_1.z.string().default("cslot-defaultkey"),
36
38
  body_limit_mb: zod_1.z.number().positive().default(512)
37
39
  })
38
40
  .default({
39
41
  host: "127.0.0.1",
40
- port: 4389,
42
+ port: 4399,
41
43
  api_key: "cslot-defaultkey",
42
44
  body_limit_mb: 512
43
45
  }),
@@ -55,11 +57,14 @@ const configSchema = zod_1.z.object({
55
57
  accounts: zod_1.z.array(managedAccountSchema).default([])
56
58
  });
57
59
  /**
58
- * 生成默认的本地 API Key,用于首次初始化配置时避免使用固定常量。
60
+ * 生成新的本地服务 API Key
61
+ *
62
+ * 该 key 仅用于本地代理服务与受管 `~/.codex/config.toml` 之间的鉴权,
63
+ * 不会影响上游官方 access token。
59
64
  *
60
65
  * @returns 随机生成的本地 API Key。
61
66
  */
62
- function generateDefaultLocalApiKey() {
67
+ function generateServerApiKey() {
63
68
  return `cslot-${node_crypto_1.default.randomBytes(18).toString("hex")}`;
64
69
  }
65
70
  /**
@@ -124,12 +129,12 @@ function expandHome(input) {
124
129
  function loadConfig() {
125
130
  const configPath = getConfigPath();
126
131
  if (!node_fs_1.default.existsSync(configPath)) {
127
- const defaultApiKey = generateDefaultLocalApiKey();
132
+ const defaultApiKey = generateServerApiKey();
128
133
  const defaultConfig = {
129
134
  version: 1,
130
135
  server: {
131
136
  host: "127.0.0.1",
132
- port: 4389,
137
+ port: 4399,
133
138
  api_key: defaultApiKey,
134
139
  body_limit_mb: 512
135
140
  },
@@ -149,12 +154,13 @@ function loadConfig() {
149
154
  let changed = JSON.stringify(parsed) !== JSON.stringify(normalized);
150
155
  if ((!parsed || typeof parsed !== "object" || !("server" in parsed)) ||
151
156
  !(parsed.server && typeof parsed.server === "object" && "api_key" in parsed.server)) {
152
- normalized.server.api_key = generateDefaultLocalApiKey();
157
+ normalized.server.api_key = generateServerApiKey();
153
158
  changed = true;
154
159
  }
155
- // 兼容历史默认值,统一迁移到新的简短本地 key。
156
- if (normalized.server.api_key === "local-only-key") {
157
- normalized.server.api_key = "cslot-defaultkey";
160
+ // 兼容历史默认值,统一迁移到新的随机本地 key。
161
+ if (normalized.server.api_key === "local-only-key" ||
162
+ normalized.server.api_key === "cslot-defaultkey") {
163
+ normalized.server.api_key = generateServerApiKey();
158
164
  changed = true;
159
165
  }
160
166
  // 当旧配置缺少新字段时,将补全后的配置回写,便于用户直接编辑查看。
@@ -175,6 +181,25 @@ function saveConfig(config) {
175
181
  const text = yaml_1.default.stringify(config);
176
182
  node_fs_1.default.writeFileSync(configPath, text, "utf8");
177
183
  }
184
+ /**
185
+ * 刷新本地代理服务 API Key,并将结果写回配置文件。
186
+ *
187
+ * 业务语义:
188
+ * 1. 每次真正启动本地代理前都重新生成一个新的本地 key。
189
+ * 2. 该 key 会同时驱动本地服务鉴权与 `~/.codex/config.toml` 中的 provider 头。
190
+ * 3. 若调用方已经持有最新配置对象,可直接传入,避免重复读取磁盘。
191
+ *
192
+ * @param config 可选的当前配置对象;未传入时会自动从磁盘读取。
193
+ * @returns 已写回磁盘的最新配置对象,其中 `server.api_key` 一定是新值。
194
+ * @throws 当配置读写失败时抛出文件系统错误。
195
+ */
196
+ function rotateServerApiKey(config) {
197
+ const nextConfig = config ?? loadConfig();
198
+ // 每次启动前轮换本地鉴权 key,避免长期复用同一个静态口令。
199
+ nextConfig.server.api_key = generateServerApiKey();
200
+ saveConfig(nextConfig);
201
+ return nextConfig;
202
+ }
178
203
  /**
179
204
  * 根据账号标识生成其独立的 HOME 目录。
180
205
  *
@@ -14,7 +14,7 @@ const text_1 = require("./text");
14
14
  */
15
15
  async function handleStart(portOverride) {
16
16
  const config = (0, config_1.loadConfig)();
17
- const result = (0, service_lifecycle_service_1.startManagedService)(portOverride);
17
+ const result = await (0, service_lifecycle_service_1.startManagedService)(portOverride);
18
18
  if (result.alreadyRunning) {
19
19
  console.log((0, text_1.bi)(`服务已在运行,PID=${result.pid}`, `Service is already running. PID=${result.pid}`));
20
20
  if (portOverride) {
@@ -23,6 +23,12 @@ async function handleStart(portOverride) {
23
23
  }
24
24
  return;
25
25
  }
26
+ if (result.autoSwitched) {
27
+ console.log((0, text_1.bi)(`默认端口 4399 已被占用,已自动切换到 ${result.port}`, `Default port 4399 is busy. Automatically switched to ${result.port}`));
28
+ }
29
+ if (result.apiKeyRotated) {
30
+ console.log((0, text_1.bi)("本次启动已重新生成本地 api_key,并同步写入受管配置。", "A new local api_key was generated for this start and synced to the managed config."));
31
+ }
26
32
  console.log((0, text_1.bi)(`服务已启动: http://${config.server.host}:${result.port}`, `Service started: http://${config.server.host}:${result.port}`));
27
33
  console.log(`PID: ${result.pid}`);
28
34
  console.log((0, text_1.bi)(`日志: ${result.logPath}`, `Log: ${result.logPath}`));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codex-slot",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "本地 Codex 多账号切换与状态管理工具",
5
5
  "type": "commonjs",
6
6
  "main": "dist/cli.js",