codex-to-im 1.0.28 → 1.0.30

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/dist/cli.mjs CHANGED
@@ -20,6 +20,14 @@ var DEFAULT_WORKSPACE_ROOT = path.join(os.homedir(), "cx2im");
20
20
  var CTI_HOME = process.env.CTI_HOME || DEFAULT_CTI_HOME;
21
21
  var CONFIG_PATH = path.join(CTI_HOME, "config.env");
22
22
  var CONFIG_V2_PATH = path.join(CTI_HOME, "config.v2.json");
23
+ function expandHomePath(value) {
24
+ if (!value) return value;
25
+ if (value === "~") return os.homedir();
26
+ if (value.startsWith("~/") || value.startsWith("~\\")) {
27
+ return path.join(os.homedir(), value.slice(2));
28
+ }
29
+ return value;
30
+ }
23
31
  function parseEnvFile(content) {
24
32
  const entries = /* @__PURE__ */ new Map();
25
33
  for (const line of content.split("\n")) {
@@ -43,6 +51,175 @@ function loadRawConfigEnv() {
43
51
  return /* @__PURE__ */ new Map();
44
52
  }
45
53
  }
54
+ function splitCsv(value) {
55
+ if (!value) return void 0;
56
+ return value.split(",").map((s) => s.trim()).filter(Boolean);
57
+ }
58
+ function parsePositiveInt(value) {
59
+ if (!value) return void 0;
60
+ const parsed = Number(value);
61
+ if (!Number.isFinite(parsed) || parsed <= 0) return void 0;
62
+ return Math.floor(parsed);
63
+ }
64
+ function parseSandboxMode(value) {
65
+ if (value === "read-only" || value === "workspace-write" || value === "danger-full-access") {
66
+ return value;
67
+ }
68
+ return void 0;
69
+ }
70
+ function parseReasoningEffort(value) {
71
+ if (value === "minimal" || value === "low" || value === "medium" || value === "high" || value === "xhigh") {
72
+ return value;
73
+ }
74
+ return void 0;
75
+ }
76
+ function normalizeFeishuSite(value) {
77
+ const normalized = (value || "").trim().replace(/\/+$/, "").toLowerCase();
78
+ if (!normalized) return "feishu";
79
+ if (normalized === "lark") return "lark";
80
+ if (normalized === "feishu") return "feishu";
81
+ if (normalized.includes("open.larksuite.com")) return "lark";
82
+ return "feishu";
83
+ }
84
+ function nowIso() {
85
+ return (/* @__PURE__ */ new Date()).toISOString();
86
+ }
87
+ function ensureConfigDir() {
88
+ fs.mkdirSync(CTI_HOME, { recursive: true });
89
+ }
90
+ function readConfigV2File() {
91
+ try {
92
+ const parsed = JSON.parse(fs.readFileSync(CONFIG_V2_PATH, "utf-8"));
93
+ if (parsed && parsed.schemaVersion === 2 && parsed.runtime && Array.isArray(parsed.channels)) {
94
+ return parsed;
95
+ }
96
+ return null;
97
+ } catch {
98
+ return null;
99
+ }
100
+ }
101
+ function writeConfigV2File(config) {
102
+ ensureConfigDir();
103
+ const tmpPath = CONFIG_V2_PATH + ".tmp";
104
+ fs.writeFileSync(tmpPath, JSON.stringify(config, null, 2), { mode: 384 });
105
+ fs.renameSync(tmpPath, CONFIG_V2_PATH);
106
+ }
107
+ function defaultAliasForProvider(provider) {
108
+ return provider === "feishu" ? "\u98DE\u4E66" : "\u5FAE\u4FE1";
109
+ }
110
+ function buildDefaultChannelId(provider) {
111
+ return `${provider}-default`;
112
+ }
113
+ function migrateLegacyEnvToV2(env) {
114
+ const rawRuntime = env.get("CTI_RUNTIME") || "codex";
115
+ const runtime = ["claude", "codex", "auto"].includes(rawRuntime) ? rawRuntime : "codex";
116
+ const enabledChannels = splitCsv(env.get("CTI_ENABLED_CHANNELS")) ?? ["feishu"];
117
+ const timestamp = nowIso();
118
+ const channels = [];
119
+ const hasFeishuConfig = Boolean(
120
+ env.get("CTI_FEISHU_APP_ID") || env.get("CTI_FEISHU_APP_SECRET") || env.get("CTI_FEISHU_ALLOWED_USERS") || enabledChannels.includes("feishu")
121
+ );
122
+ if (hasFeishuConfig) {
123
+ channels.push({
124
+ id: buildDefaultChannelId("feishu"),
125
+ alias: defaultAliasForProvider("feishu"),
126
+ provider: "feishu",
127
+ enabled: enabledChannels.includes("feishu"),
128
+ createdAt: timestamp,
129
+ updatedAt: timestamp,
130
+ config: {
131
+ appId: env.get("CTI_FEISHU_APP_ID") || void 0,
132
+ appSecret: env.get("CTI_FEISHU_APP_SECRET") || void 0,
133
+ site: normalizeFeishuSite(env.get("CTI_FEISHU_SITE") || env.get("CTI_FEISHU_DOMAIN")),
134
+ allowedUsers: splitCsv(env.get("CTI_FEISHU_ALLOWED_USERS")),
135
+ streamingEnabled: env.has("CTI_FEISHU_STREAMING_ENABLED") ? env.get("CTI_FEISHU_STREAMING_ENABLED") === "true" : true,
136
+ feedbackMarkdownEnabled: env.has("CTI_FEISHU_COMMAND_MARKDOWN_ENABLED") ? env.get("CTI_FEISHU_COMMAND_MARKDOWN_ENABLED") === "true" : true
137
+ }
138
+ });
139
+ }
140
+ const hasWeixinConfig = Boolean(
141
+ env.get("CTI_WEIXIN_BASE_URL") || env.get("CTI_WEIXIN_CDN_BASE_URL") || env.get("CTI_WEIXIN_MEDIA_ENABLED") || enabledChannels.includes("weixin")
142
+ );
143
+ if (hasWeixinConfig) {
144
+ channels.push({
145
+ id: buildDefaultChannelId("weixin"),
146
+ alias: defaultAliasForProvider("weixin"),
147
+ provider: "weixin",
148
+ enabled: enabledChannels.includes("weixin"),
149
+ createdAt: timestamp,
150
+ updatedAt: timestamp,
151
+ config: {
152
+ baseUrl: env.get("CTI_WEIXIN_BASE_URL") || void 0,
153
+ cdnBaseUrl: env.get("CTI_WEIXIN_CDN_BASE_URL") || void 0,
154
+ mediaEnabled: env.has("CTI_WEIXIN_MEDIA_ENABLED") ? env.get("CTI_WEIXIN_MEDIA_ENABLED") === "true" : void 0,
155
+ feedbackMarkdownEnabled: env.has("CTI_WEIXIN_COMMAND_MARKDOWN_ENABLED") ? env.get("CTI_WEIXIN_COMMAND_MARKDOWN_ENABLED") === "true" : false
156
+ }
157
+ });
158
+ }
159
+ return {
160
+ schemaVersion: 2,
161
+ runtime: {
162
+ provider: runtime,
163
+ defaultWorkspaceRoot: expandHomePath(env.get("CTI_DEFAULT_WORKSPACE_ROOT")) || void 0,
164
+ defaultModel: env.get("CTI_DEFAULT_MODEL") || void 0,
165
+ defaultMode: env.get("CTI_DEFAULT_MODE") || "code",
166
+ historyMessageLimit: parsePositiveInt(env.get("CTI_HISTORY_MESSAGE_LIMIT")) ?? 8,
167
+ codexSkipGitRepoCheck: env.has("CTI_CODEX_SKIP_GIT_REPO_CHECK") ? env.get("CTI_CODEX_SKIP_GIT_REPO_CHECK") === "true" : true,
168
+ codexSandboxMode: parseSandboxMode(env.get("CTI_CODEX_SANDBOX_MODE")) ?? "workspace-write",
169
+ codexReasoningEffort: parseReasoningEffort(env.get("CTI_CODEX_REASONING_EFFORT")) ?? "medium",
170
+ uiAllowLan: env.get("CTI_UI_ALLOW_LAN") === "true",
171
+ uiAccessToken: env.get("CTI_UI_ACCESS_TOKEN") || void 0,
172
+ autoApprove: env.get("CTI_AUTO_APPROVE") === "true"
173
+ },
174
+ channels
175
+ };
176
+ }
177
+ function expandConfig(v2) {
178
+ return {
179
+ schemaVersion: 2,
180
+ channels: v2.channels,
181
+ runtime: v2.runtime.provider,
182
+ enabledChannels: Array.from(new Set(
183
+ v2.channels.filter((channel) => channel.enabled).map((channel) => channel.provider)
184
+ )),
185
+ defaultWorkspaceRoot: v2.runtime.defaultWorkspaceRoot,
186
+ defaultModel: v2.runtime.defaultModel,
187
+ defaultMode: v2.runtime.defaultMode || "code",
188
+ historyMessageLimit: v2.runtime.historyMessageLimit ?? 8,
189
+ codexSkipGitRepoCheck: v2.runtime.codexSkipGitRepoCheck ?? true,
190
+ codexSandboxMode: v2.runtime.codexSandboxMode ?? "workspace-write",
191
+ codexReasoningEffort: v2.runtime.codexReasoningEffort ?? "medium",
192
+ uiAllowLan: v2.runtime.uiAllowLan === true,
193
+ uiAccessToken: v2.runtime.uiAccessToken || void 0,
194
+ autoApprove: v2.runtime.autoApprove === true
195
+ };
196
+ }
197
+ function loadConfig() {
198
+ const current = readConfigV2File();
199
+ if (current) return expandConfig(current);
200
+ const legacyEnv = loadRawConfigEnv();
201
+ if (legacyEnv.size > 0) {
202
+ const migrated = migrateLegacyEnvToV2(legacyEnv);
203
+ writeConfigV2File(migrated);
204
+ return expandConfig(migrated);
205
+ }
206
+ const empty = {
207
+ schemaVersion: 2,
208
+ runtime: {
209
+ provider: "codex",
210
+ defaultWorkspaceRoot: DEFAULT_WORKSPACE_ROOT,
211
+ defaultMode: "code",
212
+ historyMessageLimit: 8,
213
+ codexSkipGitRepoCheck: true,
214
+ codexSandboxMode: "workspace-write",
215
+ codexReasoningEffort: "medium",
216
+ uiAllowLan: false,
217
+ autoApprove: false
218
+ },
219
+ channels: []
220
+ };
221
+ return expandConfig(empty);
222
+ }
46
223
 
47
224
  // src/service-manager.ts
48
225
  var moduleDir = path2.dirname(fileURLToPath(import.meta.url));
@@ -86,6 +263,30 @@ function isProcessAlive(pid) {
86
263
  return false;
87
264
  }
88
265
  }
266
+ function collectTrackedBridgePids(bridgePid, statusPid) {
267
+ const unique = /* @__PURE__ */ new Set();
268
+ for (const pid of [bridgePid, statusPid]) {
269
+ if (Number.isFinite(pid) && pid > 0) {
270
+ unique.add(pid);
271
+ }
272
+ }
273
+ return [...unique];
274
+ }
275
+ function resolveTrackedBridgePid(bridgePid, statusPid, isAlive = isProcessAlive) {
276
+ if (isAlive(bridgePid)) return bridgePid;
277
+ if (isAlive(statusPid)) return statusPid;
278
+ return bridgePid ?? statusPid;
279
+ }
280
+ function getTrackedBridgePids(status) {
281
+ const resolvedStatus = status ?? readJsonFile(bridgeStatusFile, { running: false });
282
+ return collectTrackedBridgePids(readPid(bridgePidFile), resolvedStatus.pid);
283
+ }
284
+ function clearBridgePidFile() {
285
+ try {
286
+ fs2.unlinkSync(bridgePidFile);
287
+ } catch {
288
+ }
289
+ }
89
290
  function sleep(ms) {
90
291
  return new Promise((resolve) => setTimeout(resolve, ms));
91
292
  }
@@ -246,7 +447,7 @@ function getCurrentUiServerUrl() {
246
447
  }
247
448
  function getBridgeStatus() {
248
449
  const status = readJsonFile(bridgeStatusFile, { running: false });
249
- const pid = readPid(bridgePidFile) ?? status.pid;
450
+ const pid = resolveTrackedBridgePid(readPid(bridgePidFile), status.pid);
250
451
  if (!isProcessAlive(pid)) {
251
452
  return {
252
453
  ...status,
@@ -284,6 +485,27 @@ function buildDaemonEnv() {
284
485
  delete env.CLAUDECODE;
285
486
  return env;
286
487
  }
488
+ function describeBridgeStartupPreflightFailure(channels) {
489
+ const configured = Array.isArray(channels) ? channels : [];
490
+ if (configured.length === 0) {
491
+ return "\u672A\u914D\u7F6E\u4EFB\u4F55\u901A\u9053\u5B9E\u4F8B\u3002\u8BF7\u5148\u5728 Web \u63A7\u5236\u53F0\u521B\u5EFA\u5E76\u4FDD\u5B58\u81F3\u5C11\u4E00\u4E2A\u98DE\u4E66\u6216\u5FAE\u4FE1\u901A\u9053\uFF0C\u7136\u540E\u518D\u542F\u52A8\u6865\u63A5\u670D\u52A1\u3002";
492
+ }
493
+ const enabled = configured.filter((channel) => channel.enabled !== false);
494
+ if (enabled.length === 0) {
495
+ return "\u5F53\u524D\u6240\u6709\u901A\u9053\u5B9E\u4F8B\u90FD\u5DF2\u7981\u7528\u3002\u8BF7\u5148\u542F\u7528\u81F3\u5C11\u4E00\u4E2A\u901A\u9053\u5B9E\u4F8B\uFF0C\u7136\u540E\u518D\u542F\u52A8\u6865\u63A5\u670D\u52A1\u3002";
496
+ }
497
+ return null;
498
+ }
499
+ function describeBridgeActivationFailure(status, channels) {
500
+ const statusReason = status.lastExitReason?.trim();
501
+ if (statusReason) return statusReason;
502
+ const preflightFailure = describeBridgeStartupPreflightFailure(channels);
503
+ if (preflightFailure) return preflightFailure;
504
+ const enabled = (channels || []).filter((channel) => channel.enabled !== false);
505
+ if (enabled.length === 0) return null;
506
+ const labels = enabled.map((channel) => channel.alias?.trim() || channel.id).join("\u3001");
507
+ return `\u6CA1\u6709\u4EFB\u4F55\u901A\u9053\u9002\u914D\u5668\u542F\u52A8\u6210\u529F\u3002\u8BF7\u68C0\u67E5\u901A\u9053\u914D\u7F6E\u3001\u51ED\u636E\u548C\u65E5\u5FD7\u3002\u5F53\u524D\u5DF2\u542F\u7528\u901A\u9053\uFF1A${labels}`;
508
+ }
287
509
  async function waitForBridgeRunning(timeoutMs = 2e4) {
288
510
  const startedAt = Date.now();
289
511
  while (Date.now() - startedAt < timeoutMs) {
@@ -311,7 +533,16 @@ async function waitForUiServer(timeoutMs = 15e3) {
311
533
  async function startBridge() {
312
534
  ensureDirs();
313
535
  const current = getBridgeStatus();
314
- if (current.running) return current;
536
+ const extraAlivePids = getTrackedBridgePids(current).filter((pid) => pid !== current.pid && isProcessAlive(pid));
537
+ if (current.running && extraAlivePids.length === 0) return current;
538
+ if (current.running && extraAlivePids.length > 0) {
539
+ await stopBridge();
540
+ }
541
+ const config = loadConfig();
542
+ const preflightFailure = describeBridgeStartupPreflightFailure(config.channels);
543
+ if (preflightFailure) {
544
+ throw new Error(preflightFailure);
545
+ }
315
546
  const daemonEntry = path2.join(packageRoot, "dist", "daemon.mjs");
316
547
  if (!fs2.existsSync(daemonEntry)) {
317
548
  throw new Error(`Daemon bundle not found at ${daemonEntry}. Run npm run build first.`);
@@ -328,36 +559,45 @@ async function startBridge() {
328
559
  child.unref();
329
560
  const status = await waitForBridgeRunning();
330
561
  if (!status.running) {
331
- throw new Error(status.lastExitReason || "Bridge failed to report running=true.");
562
+ throw new Error(
563
+ describeBridgeActivationFailure(status, config.channels) || "Bridge failed to report running=true."
564
+ );
332
565
  }
333
566
  return status;
334
567
  }
335
568
  async function stopBridge() {
336
- const status = getBridgeStatus();
337
- if (!status.pid || !isProcessAlive(status.pid)) {
338
- return { ...status, running: false };
339
- }
340
- if (process.platform === "win32") {
341
- await new Promise((resolve) => {
342
- const killer = spawn("cmd", ["/c", "taskkill", "/PID", String(status.pid), "/T", "/F"], {
343
- stdio: "ignore",
344
- ...WINDOWS_HIDE
569
+ const status = readJsonFile(bridgeStatusFile, { running: false });
570
+ const pids = getTrackedBridgePids(status).filter((pid) => isProcessAlive(pid));
571
+ if (pids.length === 0) {
572
+ clearBridgePidFile();
573
+ return { ...getBridgeStatus(), running: false };
574
+ }
575
+ for (const pid of pids) {
576
+ if (process.platform === "win32") {
577
+ await new Promise((resolve) => {
578
+ const killer = spawn("cmd", ["/c", "taskkill", "/PID", String(pid), "/T", "/F"], {
579
+ stdio: "ignore",
580
+ ...WINDOWS_HIDE
581
+ });
582
+ killer.on("exit", () => resolve());
583
+ killer.on("error", () => resolve());
345
584
  });
346
- killer.on("exit", () => resolve());
347
- killer.on("error", () => resolve());
348
- });
349
- } else {
350
- try {
351
- process.kill(status.pid, "SIGTERM");
352
- } catch {
585
+ } else {
586
+ try {
587
+ process.kill(pid, "SIGTERM");
588
+ } catch {
589
+ }
353
590
  }
354
591
  }
355
592
  const startedAt = Date.now();
356
593
  while (Date.now() - startedAt < 1e4) {
357
- const next = getBridgeStatus();
358
- if (!next.running) return next;
594
+ if (pids.every((pid) => !isProcessAlive(pid))) {
595
+ clearBridgePidFile();
596
+ return getBridgeStatus();
597
+ }
359
598
  await sleep(300);
360
599
  }
600
+ clearBridgePidFile();
361
601
  return getBridgeStatus();
362
602
  }
363
603
  async function getBridgeAutostartStatus() {