codexmate 0.0.15 → 0.0.17

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/cli.js CHANGED
@@ -123,6 +123,7 @@ const MAX_UPLOAD_SIZE = 200 * 1024 * 1024;
123
123
  const MAX_SKILLS_ZIP_UPLOAD_SIZE = 20 * 1024 * 1024;
124
124
  const MAX_SKILLS_ZIP_ENTRY_COUNT = 2000;
125
125
  const MAX_SKILLS_ZIP_UNCOMPRESSED_BYTES = 512 * 1024 * 1024;
126
+ const DEFAULT_EXTRACT_SUFFIXES = Object.freeze(['.json']);
126
127
  const DOWNLOAD_ARTIFACT_TTL_MS = 10 * 60 * 1000;
127
128
  const g_downloadArtifacts = new Map();
128
129
  const BUILTIN_PROXY_PROVIDER_NAME = 'codexmate-proxy';
@@ -5890,8 +5891,15 @@ function buildProviderSharePayload(params = {}) {
5890
5891
 
5891
5892
  const baseUrl = typeof provider.base_url === 'string' ? provider.base_url.trim() : '';
5892
5893
  const apiKey = typeof provider.preferred_auth_method === 'string'
5893
- ? provider.preferred_auth_method
5894
+ ? provider.preferred_auth_method.trim()
5895
+ : '';
5896
+ const currentModels = readCurrentModels();
5897
+ const savedModel = currentModels && typeof currentModels[name] === 'string'
5898
+ ? currentModels[name].trim()
5894
5899
  : '';
5900
+ const activeProvider = typeof config.model_provider === 'string' ? config.model_provider.trim() : '';
5901
+ const activeModel = typeof config.model === 'string' ? config.model.trim() : '';
5902
+ const model = savedModel || (activeProvider === name ? activeModel : '');
5895
5903
 
5896
5904
  if (!baseUrl) {
5897
5905
  return { error: `提供商 ${name} 缺少 base_url` };
@@ -5901,7 +5909,8 @@ function buildProviderSharePayload(params = {}) {
5901
5909
  payload: {
5902
5910
  name,
5903
5911
  baseUrl,
5904
- apiKey
5912
+ apiKey,
5913
+ model
5905
5914
  }
5906
5915
  };
5907
5916
  }
@@ -6928,6 +6937,8 @@ function readClaudeSettingsInfo() {
6928
6937
  exists: !!readResult.exists,
6929
6938
  targetPath: CLAUDE_SETTINGS_FILE,
6930
6939
  apiKey: typeof env.ANTHROPIC_API_KEY === 'string' ? env.ANTHROPIC_API_KEY : '',
6940
+ authToken: typeof env.ANTHROPIC_AUTH_TOKEN === 'string' ? env.ANTHROPIC_AUTH_TOKEN : '',
6941
+ useKey: typeof env.CLAUDE_CODE_USE_KEY === 'string' ? env.CLAUDE_CODE_USE_KEY : '',
6931
6942
  baseUrl: typeof env.ANTHROPIC_BASE_URL === 'string' ? env.ANTHROPIC_BASE_URL : '',
6932
6943
  model: typeof env.ANTHROPIC_MODEL === 'string' ? env.ANTHROPIC_MODEL : '',
6933
6944
  env
@@ -7784,6 +7795,318 @@ async function cmdUnzip(zipPath, outputDir) {
7784
7795
  }
7785
7796
  }
7786
7797
 
7798
+ function splitExtractSuffixInput(rawValue) {
7799
+ if (Array.isArray(rawValue)) {
7800
+ return rawValue.flatMap((item) => splitExtractSuffixInput(item));
7801
+ }
7802
+ if (typeof rawValue !== 'string') {
7803
+ return [];
7804
+ }
7805
+ return rawValue
7806
+ .split(/[,\s]+/g)
7807
+ .map((item) => item.trim())
7808
+ .filter(Boolean);
7809
+ }
7810
+
7811
+ function normalizeExtractSuffix(rawSuffix, fallbackSuffixes = DEFAULT_EXTRACT_SUFFIXES) {
7812
+ const fallbackItems = splitExtractSuffixInput(fallbackSuffixes);
7813
+ const sourceItems = splitExtractSuffixInput(rawSuffix);
7814
+ const source = sourceItems.length > 0 ? sourceItems : fallbackItems;
7815
+ const dedup = new Set();
7816
+
7817
+ for (const item of source) {
7818
+ const lower = item.toLowerCase();
7819
+ if (!lower) {
7820
+ continue;
7821
+ }
7822
+ const normalized = lower.startsWith('.') ? lower : `.${lower}`;
7823
+ if (normalized.length > 1) {
7824
+ dedup.add(normalized);
7825
+ }
7826
+ }
7827
+
7828
+ if (dedup.size === 0) {
7829
+ return [...DEFAULT_EXTRACT_SUFFIXES];
7830
+ }
7831
+ return Array.from(dedup);
7832
+ }
7833
+
7834
+ function buildDefaultExtractOutputDir(baseCwd = process.cwd()) {
7835
+ const normalizedCwd = path.resolve(baseCwd);
7836
+ const parentDir = path.dirname(normalizedCwd);
7837
+ const timestamp = formatTimestampForFileName().replace(/-/g, '');
7838
+ return path.join(parentDir, timestamp);
7839
+ }
7840
+
7841
+ function sanitizeNameSegment(rawValue, fallback = 'item') {
7842
+ const value = typeof rawValue === 'string' ? rawValue.trim() : '';
7843
+ const sanitized = value
7844
+ .replace(/[^\w.-]+/g, '_')
7845
+ .replace(/^_+|_+$/g, '');
7846
+ return sanitized || fallback;
7847
+ }
7848
+
7849
+ function resolveDuplicateOutputPath(outputDir, originalFileName, zipPath = '', counters = new Map()) {
7850
+ const fallbackName = `file${path.extname(originalFileName || '')}`;
7851
+ const fileName = path.basename(originalFileName || '') || fallbackName;
7852
+ const firstChoice = path.join(outputDir, fileName);
7853
+ const firstChoiceKey = `exact:${fileName}`;
7854
+ if (!counters.has(firstChoiceKey)) {
7855
+ counters.set(firstChoiceKey, true);
7856
+ if (!fs.existsSync(firstChoice)) {
7857
+ return firstChoice;
7858
+ }
7859
+ }
7860
+
7861
+ const ext = path.extname(fileName);
7862
+ const baseName = path.basename(fileName, ext);
7863
+ const safeBaseName = sanitizeNameSegment(baseName, 'file');
7864
+ const zipBaseName = sanitizeNameSegment(path.basename(zipPath || '', '.zip'), 'zip');
7865
+ const duplicateKey = `dup:${safeBaseName}|${zipBaseName}|${ext}`;
7866
+ let index = counters.has(duplicateKey) ? counters.get(duplicateKey) : 1;
7867
+
7868
+ for (; index <= 100000; index++) {
7869
+ const candidateName = `${safeBaseName}__${zipBaseName}__${index}${ext}`;
7870
+ const candidatePath = path.join(outputDir, candidateName);
7871
+ if (!fs.existsSync(candidatePath)) {
7872
+ counters.set(duplicateKey, index + 1);
7873
+ return candidatePath;
7874
+ }
7875
+ }
7876
+
7877
+ throw new Error(`重名文件过多,无法生成唯一文件名: ${fileName}`);
7878
+ }
7879
+
7880
+ function collectZipFilesFromDir(rootDir, recursive = true) {
7881
+ const queue = [rootDir];
7882
+ const result = [];
7883
+
7884
+ while (queue.length > 0) {
7885
+ const currentDir = queue.shift();
7886
+ let entries = [];
7887
+ try {
7888
+ entries = fs.readdirSync(currentDir, { withFileTypes: true });
7889
+ } catch (e) {
7890
+ throw new Error(`读取目录失败: ${currentDir} (${e.message})`);
7891
+ }
7892
+
7893
+ for (const entry of entries) {
7894
+ const entryPath = path.join(currentDir, entry.name);
7895
+ if (entry.isDirectory()) {
7896
+ if (recursive) {
7897
+ queue.push(entryPath);
7898
+ }
7899
+ continue;
7900
+ }
7901
+ if (entry.isFile() && entry.name.toLowerCase().endsWith('.zip')) {
7902
+ result.push(entryPath);
7903
+ }
7904
+ }
7905
+ }
7906
+
7907
+ result.sort((a, b) => a.localeCompare(b));
7908
+ return result;
7909
+ }
7910
+
7911
+ function extractMatchedEntriesFromZip(zipPath, outputDir, suffixes, duplicateCounters = new Map()) {
7912
+ const normalizedSuffixes = normalizeExtractSuffix(suffixes);
7913
+ return new Promise((resolve, reject) => {
7914
+ yauzl.open(zipPath, { lazyEntries: true, autoClose: false }, (openErr, zipFile) => {
7915
+ if (openErr) {
7916
+ reject(openErr);
7917
+ return;
7918
+ }
7919
+ if (!zipFile) {
7920
+ reject(new Error('无法读取 ZIP 文件'));
7921
+ return;
7922
+ }
7923
+
7924
+ let settled = false;
7925
+ let matched = 0;
7926
+ let extracted = 0;
7927
+ let skippedDir = 0;
7928
+ let skippedExt = 0;
7929
+
7930
+ const finish = (err) => {
7931
+ if (settled) return;
7932
+ settled = true;
7933
+ try {
7934
+ zipFile.close();
7935
+ } catch (_) {}
7936
+ if (err) {
7937
+ reject(err);
7938
+ } else {
7939
+ resolve({ matched, extracted, skippedDir, skippedExt });
7940
+ }
7941
+ };
7942
+
7943
+ zipFile.on('entry', (entry) => {
7944
+ if (settled) return;
7945
+ const rawEntryName = typeof entry.fileName === 'string' ? entry.fileName : '';
7946
+ const normalizedEntryName = rawEntryName.replace(/\\/g, '/');
7947
+
7948
+ if (!normalizedEntryName || normalizedEntryName.endsWith('/')) {
7949
+ skippedDir += 1;
7950
+ zipFile.readEntry();
7951
+ return;
7952
+ }
7953
+
7954
+ const entryBaseName = path.basename(normalizedEntryName);
7955
+ const lowerBaseName = entryBaseName.toLowerCase();
7956
+ const matchedSuffix = normalizedSuffixes.some((suffix) => lowerBaseName.endsWith(suffix));
7957
+ if (!entryBaseName || !matchedSuffix) {
7958
+ skippedExt += 1;
7959
+ zipFile.readEntry();
7960
+ return;
7961
+ }
7962
+
7963
+ matched += 1;
7964
+ zipFile.openReadStream(entry, (streamErr, readStream) => {
7965
+ if (streamErr || !readStream) {
7966
+ finish(streamErr || new Error('无法读取 ZIP 条目流'));
7967
+ return;
7968
+ }
7969
+
7970
+ let completed = false;
7971
+ const outputPath = resolveDuplicateOutputPath(outputDir, entryBaseName, zipPath, duplicateCounters);
7972
+ const writeStream = fs.createWriteStream(outputPath);
7973
+ const fail = (writeErr) => {
7974
+ if (completed) return;
7975
+ completed = true;
7976
+ try {
7977
+ readStream.destroy();
7978
+ } catch (_) {}
7979
+ try {
7980
+ writeStream.destroy();
7981
+ } catch (_) {}
7982
+ try {
7983
+ if (fs.existsSync(outputPath)) {
7984
+ fs.unlinkSync(outputPath);
7985
+ }
7986
+ } catch (_) {}
7987
+ finish(writeErr);
7988
+ };
7989
+
7990
+ readStream.on('error', fail);
7991
+ writeStream.on('error', fail);
7992
+ writeStream.on('finish', () => {
7993
+ if (completed || settled) return;
7994
+ completed = true;
7995
+ extracted += 1;
7996
+ zipFile.readEntry();
7997
+ });
7998
+
7999
+ readStream.pipe(writeStream);
8000
+ });
8001
+ });
8002
+
8003
+ zipFile.on('end', () => {
8004
+ finish(null);
8005
+ });
8006
+ zipFile.on('error', (zipErr) => {
8007
+ finish(zipErr);
8008
+ });
8009
+
8010
+ zipFile.readEntry();
8011
+ });
8012
+ });
8013
+ }
8014
+
8015
+ async function cmdUnzipExt(zipDirPath, outputDir, options = {}) {
8016
+ if (!zipDirPath) {
8017
+ console.error('用法: codexmate unzip-ext <zip目录> [输出目录] [--ext:后缀[,后缀...]] [--no-recursive]');
8018
+ console.log('\n示例:');
8019
+ console.log(' codexmate unzip-ext ./archives');
8020
+ console.log(' codexmate unzip-ext ./archives ./output --ext:json,txt');
8021
+ console.log(' codexmate unzip-ext D:/data/zips --ext:txt --no-recursive');
8022
+ console.log(' 说明: 默认递归扫描子目录,可通过 --no-recursive 关闭递归');
8023
+ process.exit(1);
8024
+ }
8025
+
8026
+ const recursive = options.recursive !== false;
8027
+ const suffixes = normalizeExtractSuffix(options.ext);
8028
+ const absZipDir = path.resolve(zipDirPath);
8029
+ const absOutputDir = outputDir ? path.resolve(outputDir) : buildDefaultExtractOutputDir(process.cwd());
8030
+
8031
+ if (!fs.existsSync(absZipDir)) {
8032
+ console.error('错误: 目录不存在:', absZipDir);
8033
+ process.exit(1);
8034
+ }
8035
+ try {
8036
+ if (!fs.statSync(absZipDir).isDirectory()) {
8037
+ console.error('错误: 仅支持目录路径:', absZipDir);
8038
+ process.exit(1);
8039
+ }
8040
+ } catch (e) {
8041
+ console.error('错误: 无法读取目录信息:', e.message);
8042
+ process.exit(1);
8043
+ }
8044
+
8045
+ let zipFiles = [];
8046
+ try {
8047
+ zipFiles = collectZipFilesFromDir(absZipDir, recursive);
8048
+ } catch (e) {
8049
+ console.error('扫描 ZIP 文件失败:', e.message);
8050
+ process.exit(1);
8051
+ }
8052
+
8053
+ if (zipFiles.length === 0) {
8054
+ console.error('错误: 未找到任何 ZIP 文件');
8055
+ process.exit(1);
8056
+ }
8057
+
8058
+ ensureDir(absOutputDir);
8059
+
8060
+ console.log('\n批量解压配置:');
8061
+ console.log(' ZIP 目录:', absZipDir);
8062
+ console.log(' 输出目录:', absOutputDir);
8063
+ console.log(' 后缀过滤:', suffixes.join(', '));
8064
+ console.log(' 递归扫描:', recursive ? '是' : '否');
8065
+ console.log(' ZIP 数量:', zipFiles.length);
8066
+ console.log('\n开始提取...\n');
8067
+
8068
+ let totalMatched = 0;
8069
+ let totalExtracted = 0;
8070
+ let totalSkippedDir = 0;
8071
+ let totalSkippedExt = 0;
8072
+ const failed = [];
8073
+ const duplicateCounters = new Map();
8074
+
8075
+ for (const zipFilePath of zipFiles) {
8076
+ try {
8077
+ await inspectZipArchiveLimits(zipFilePath, {
8078
+ maxEntryCount: MAX_SKILLS_ZIP_ENTRY_COUNT,
8079
+ maxUncompressedBytes: MAX_SKILLS_ZIP_UNCOMPRESSED_BYTES
8080
+ });
8081
+ const result = await extractMatchedEntriesFromZip(zipFilePath, absOutputDir, suffixes, duplicateCounters);
8082
+ totalMatched += result.matched;
8083
+ totalExtracted += result.extracted;
8084
+ totalSkippedDir += result.skippedDir;
8085
+ totalSkippedExt += result.skippedExt;
8086
+ console.log(`✓ ${path.basename(zipFilePath)}: 命中 ${result.matched},提取 ${result.extracted}`);
8087
+ } catch (e) {
8088
+ failed.push({ zipFilePath, message: e && e.message ? e.message : String(e) });
8089
+ console.error(`✗ ${path.basename(zipFilePath)}: ${e && e.message ? e.message : e}`);
8090
+ }
8091
+ }
8092
+
8093
+ console.log('\n提取结果:');
8094
+ console.log(' 输出目录:', absOutputDir);
8095
+ console.log(' 扫描 ZIP:', zipFiles.length);
8096
+ console.log(' 命中条目:', totalMatched);
8097
+ console.log(' 已提取:', totalExtracted);
8098
+ console.log(' 已跳过(目录条目):', totalSkippedDir);
8099
+ console.log(' 已跳过(后缀不匹配):', totalSkippedExt);
8100
+ if (failed.length > 0) {
8101
+ console.error(' 失败数量:', failed.length);
8102
+ for (const item of failed) {
8103
+ console.error(` - ${item.zipFilePath}: ${item.message}`);
8104
+ }
8105
+ process.exit(1);
8106
+ }
8107
+ console.log();
8108
+ }
8109
+
7787
8110
  function resolveExportOutputPath(outputPath, defaultFileName) {
7788
8111
  const fallback = path.resolve(process.cwd(), defaultFileName);
7789
8112
  if (typeof outputPath !== 'string' || !outputPath.trim()) {
@@ -9233,7 +9556,317 @@ async function cmdWorkflow(args = []) {
9233
9556
  throw new Error(`未知 workflow 子命令: ${subcommand}`);
9234
9557
  }
9235
9558
 
9236
- async function runProxyCommand(displayName, binNames, args = [], installTip = '') {
9559
+ // #region parseCodexProxyOptions
9560
+ function parseCodexProxyOptions(args = []) {
9561
+ const options = {
9562
+ passthroughArgs: [],
9563
+ queuedFollowUps: []
9564
+ };
9565
+ const argv = Array.isArray(args) ? args : [];
9566
+
9567
+ const pushFollowUp = (value, optionName) => {
9568
+ const raw = value === undefined || value === null ? '' : String(value);
9569
+ if (!raw.trim()) {
9570
+ throw new Error(`${optionName} 需要提供非空内容`);
9571
+ }
9572
+ options.queuedFollowUps.push(raw);
9573
+ };
9574
+
9575
+ for (let i = 0; i < argv.length; i++) {
9576
+ const arg = argv[i];
9577
+ if (arg === undefined || arg === null) {
9578
+ continue;
9579
+ }
9580
+ const text = String(arg);
9581
+ if (text === '--') {
9582
+ options.passthroughArgs.push(...argv.slice(i).map((item) => String(item)));
9583
+ break;
9584
+ }
9585
+ if (text === '--queued-follow-up' || text === '--follow-up') {
9586
+ const next = argv[i + 1];
9587
+ if (next === undefined) {
9588
+ throw new Error(`${text} 需要提供内容`);
9589
+ }
9590
+ pushFollowUp(next, text);
9591
+ i += 1;
9592
+ continue;
9593
+ }
9594
+ if (text.startsWith('--queued-follow-up=')) {
9595
+ pushFollowUp(text.slice('--queued-follow-up='.length), '--queued-follow-up');
9596
+ continue;
9597
+ }
9598
+ if (text.startsWith('--follow-up=')) {
9599
+ pushFollowUp(text.slice('--follow-up='.length), '--follow-up');
9600
+ continue;
9601
+ }
9602
+ options.passthroughArgs.push(text);
9603
+ }
9604
+
9605
+ return options;
9606
+ }
9607
+ // #endregion parseCodexProxyOptions
9608
+
9609
+ function shellEscapePosixArg(value) {
9610
+ const text = value === undefined || value === null ? '' : String(value);
9611
+ return `'${text.replace(/'/g, `'\"'\"'`)}'`;
9612
+ }
9613
+
9614
+ // #region buildScriptCommandArgs
9615
+ function buildScriptCommandArgs(commandLine) {
9616
+ const platform = process.platform;
9617
+ // util-linux script needs -e/--return to propagate child exit code.
9618
+ if (platform === 'linux' || platform === 'android') {
9619
+ return ['-q', '-e', '-c', commandLine, '/dev/null'];
9620
+ }
9621
+ // NetBSD supports -e/-c, matching util-linux style contract.
9622
+ if (platform === 'netbsd') {
9623
+ return ['-q', '-e', '-c', commandLine, '/dev/null'];
9624
+ }
9625
+ // OpenBSD supports "-c <command>" with a trailing output file path.
9626
+ if (platform === 'openbsd') {
9627
+ return ['-c', commandLine, '/dev/null'];
9628
+ }
9629
+ // BSD/macOS script does not support util-linux "-c <cmd>" syntax.
9630
+ if (platform === 'darwin' || platform === 'freebsd') {
9631
+ return ['-q', '/dev/null', 'sh', '-lc', commandLine];
9632
+ }
9633
+ throw new Error(`当前平台暂不支持 --follow-up 自动排队(platform=${platform})`);
9634
+ }
9635
+ // #endregion buildScriptCommandArgs
9636
+
9637
+ // #region runProxyCommandWithQueuedFollowUps
9638
+ async function runProxyCommandWithQueuedFollowUps(selectedBin, finalArgs = [], queuedFollowUps = []) {
9639
+ if (!process.stdin || !process.stdin.isTTY) {
9640
+ throw new Error('当前 stdin 不是 TTY,无法使用 --follow-up 自动排队。');
9641
+ }
9642
+
9643
+ const scriptPath = resolveCommandPath('script');
9644
+ if (!scriptPath) {
9645
+ throw new Error('未找到 script 命令,无法自动注入 queued follow-up 消息。');
9646
+ }
9647
+
9648
+ const commandLine = [selectedBin, ...finalArgs].map((item) => shellEscapePosixArg(item)).join(' ');
9649
+ const scriptArgs = buildScriptCommandArgs(commandLine);
9650
+
9651
+ return new Promise((resolve, reject) => {
9652
+ let settled = false;
9653
+ const child = spawn(scriptPath, scriptArgs, {
9654
+ stdio: ['pipe', 'pipe', 'pipe']
9655
+ });
9656
+
9657
+ const stdin = process.stdin;
9658
+ const hadRawMode = !!stdin.isRaw;
9659
+ let cleanedUp = false;
9660
+ let waitingDrain = false;
9661
+ let followUpsFlushed = false;
9662
+ let outputReadyDetected = false;
9663
+ const timers = [];
9664
+ const pendingWrites = [];
9665
+ let onChildStdinDrain = null;
9666
+ let onChildStdinError = null;
9667
+ const resolveOnce = (code) => {
9668
+ if (settled) return;
9669
+ settled = true;
9670
+ resolve(code);
9671
+ };
9672
+ const rejectOnce = (error) => {
9673
+ if (settled) return;
9674
+ settled = true;
9675
+ reject(error);
9676
+ };
9677
+ const handleWriteFailure = (error) => {
9678
+ const err = error instanceof Error ? error : new Error(String(error || 'unknown'));
9679
+ cleanup();
9680
+ try {
9681
+ if (!child.killed) {
9682
+ child.kill('SIGTERM');
9683
+ }
9684
+ } catch (_) {
9685
+ // Ignore failure to terminate child after stdin write failure.
9686
+ }
9687
+ rejectOnce(new Error(`写入 ${selectedBin} stdin 失败: ${err.message}`));
9688
+ };
9689
+ const flushPendingWrites = () => {
9690
+ if (cleanedUp || child.stdin.destroyed) {
9691
+ pendingWrites.length = 0;
9692
+ return;
9693
+ }
9694
+ while (pendingWrites.length > 0) {
9695
+ const chunk = pendingWrites[0];
9696
+ let canContinue = true;
9697
+ try {
9698
+ canContinue = child.stdin.write(chunk, (error) => {
9699
+ if (error) {
9700
+ handleWriteFailure(error);
9701
+ }
9702
+ });
9703
+ } catch (error) {
9704
+ handleWriteFailure(error);
9705
+ return;
9706
+ }
9707
+ pendingWrites.shift();
9708
+ if (!canContinue) {
9709
+ waitingDrain = true;
9710
+ try {
9711
+ stdin.pause();
9712
+ } catch (_) {
9713
+ // Ignore stdin pause failures.
9714
+ }
9715
+ return;
9716
+ }
9717
+ }
9718
+ waitingDrain = false;
9719
+ try {
9720
+ stdin.resume();
9721
+ } catch (_) {
9722
+ // Ignore stdin resume failures.
9723
+ }
9724
+ };
9725
+ const enqueueWrite = (chunk) => {
9726
+ if (cleanedUp) return;
9727
+ pendingWrites.push(chunk);
9728
+ flushPendingWrites();
9729
+ };
9730
+ const onInput = (chunk) => {
9731
+ if (!child.stdin.destroyed) {
9732
+ enqueueWrite(chunk);
9733
+ }
9734
+ };
9735
+ const flushQueuedFollowUps = () => {
9736
+ if (followUpsFlushed) return;
9737
+ followUpsFlushed = true;
9738
+ queuedFollowUps.forEach((message, index) => {
9739
+ const timer = setTimeout(() => {
9740
+ if (!child.stdin.destroyed) {
9741
+ // PTY submit should use CR instead of LF.
9742
+ enqueueWrite(`${message}\r`);
9743
+ }
9744
+ }, index * 80);
9745
+ timers.push(timer);
9746
+ });
9747
+ };
9748
+ const markOutputReady = () => {
9749
+ if (outputReadyDetected) return;
9750
+ outputReadyDetected = true;
9751
+ timers.push(setTimeout(() => {
9752
+ flushQueuedFollowUps();
9753
+ }, 120));
9754
+ };
9755
+ const onStdoutData = (chunk) => {
9756
+ process.stdout.write(chunk);
9757
+ markOutputReady();
9758
+ };
9759
+ const onStderrData = (chunk) => {
9760
+ process.stderr.write(chunk);
9761
+ markOutputReady();
9762
+ };
9763
+ const onProcessExit = () => {
9764
+ cleanup();
9765
+ };
9766
+ const onProcessSigint = () => {
9767
+ cleanup();
9768
+ try {
9769
+ if (!child.killed) {
9770
+ child.kill('SIGINT');
9771
+ }
9772
+ } catch (_) {
9773
+ // Ignore forwarding failures and keep exit path deterministic.
9774
+ }
9775
+ process.exit(130);
9776
+ };
9777
+ const onProcessSigterm = () => {
9778
+ cleanup();
9779
+ try {
9780
+ if (!child.killed) {
9781
+ child.kill('SIGTERM');
9782
+ }
9783
+ } catch (_) {
9784
+ // Ignore forwarding failures and keep exit path deterministic.
9785
+ }
9786
+ process.exit(143);
9787
+ };
9788
+ const cleanup = () => {
9789
+ if (cleanedUp) return;
9790
+ cleanedUp = true;
9791
+ stdin.removeListener('data', onInput);
9792
+ process.removeListener('exit', onProcessExit);
9793
+ process.removeListener('SIGINT', onProcessSigint);
9794
+ process.removeListener('SIGTERM', onProcessSigterm);
9795
+ child.stdout.removeListener('data', onStdoutData);
9796
+ child.stderr.removeListener('data', onStderrData);
9797
+ if (onChildStdinDrain) {
9798
+ child.stdin.removeListener('drain', onChildStdinDrain);
9799
+ }
9800
+ if (onChildStdinError) {
9801
+ child.stdin.removeListener('error', onChildStdinError);
9802
+ }
9803
+ while (timers.length > 0) {
9804
+ clearTimeout(timers.pop());
9805
+ }
9806
+ try {
9807
+ if (typeof stdin.setRawMode === 'function' && !hadRawMode) {
9808
+ stdin.setRawMode(false);
9809
+ }
9810
+ } catch (_) {
9811
+ // Ignore raw mode restore failures at shutdown.
9812
+ }
9813
+ };
9814
+
9815
+ process.on('exit', onProcessExit);
9816
+ process.on('SIGINT', onProcessSigint);
9817
+ process.on('SIGTERM', onProcessSigterm);
9818
+ child.stdout.on('data', onStdoutData);
9819
+ child.stderr.on('data', onStderrData);
9820
+ onChildStdinDrain = () => {
9821
+ waitingDrain = false;
9822
+ flushPendingWrites();
9823
+ };
9824
+ onChildStdinError = (error) => {
9825
+ handleWriteFailure(error);
9826
+ };
9827
+ child.stdin.on('drain', onChildStdinDrain);
9828
+ child.stdin.on('error', onChildStdinError);
9829
+ try {
9830
+ if (typeof stdin.setRawMode === 'function' && !hadRawMode) {
9831
+ stdin.setRawMode(true);
9832
+ }
9833
+ } catch (_) {
9834
+ // Keep graceful fallback if raw mode toggle is not supported.
9835
+ }
9836
+
9837
+ stdin.resume();
9838
+ stdin.on('data', onInput);
9839
+ // Fallback in case the child stays silent before prompt render.
9840
+ timers.push(setTimeout(() => {
9841
+ flushQueuedFollowUps();
9842
+ }, 1500));
9843
+
9844
+ child.on('error', (err) => {
9845
+ cleanup();
9846
+ rejectOnce(new Error(`运行 ${selectedBin} 失败: ${err.message}`));
9847
+ });
9848
+
9849
+ child.on('close', (code, signal) => {
9850
+ cleanup();
9851
+ if (typeof code === 'number') {
9852
+ resolveOnce(code);
9853
+ return;
9854
+ }
9855
+ if (signal === 'SIGINT') {
9856
+ resolveOnce(130);
9857
+ return;
9858
+ }
9859
+ if (signal === 'SIGTERM') {
9860
+ resolveOnce(143);
9861
+ return;
9862
+ }
9863
+ resolveOnce(1);
9864
+ });
9865
+ });
9866
+ }
9867
+ // #endregion runProxyCommandWithQueuedFollowUps
9868
+
9869
+ async function runProxyCommand(displayName, binNames, args = [], installTip = '', runtimeOptions = {}) {
9237
9870
  const extraArgs = Array.isArray(args) ? args.filter(arg => arg !== undefined) : [];
9238
9871
  const hasYolo = extraArgs.includes('--yolo');
9239
9872
  const finalArgs = hasYolo ? extraArgs : ['--yolo', ...extraArgs];
@@ -9259,6 +9892,14 @@ async function runProxyCommand(displayName, binNames, args = [], installTip = ''
9259
9892
  throw new Error(msg);
9260
9893
  }
9261
9894
 
9895
+ const queuedFollowUps = runtimeOptions && Array.isArray(runtimeOptions.queuedFollowUps)
9896
+ ? runtimeOptions.queuedFollowUps.filter((item) => typeof item === 'string' && item.trim())
9897
+ : [];
9898
+
9899
+ if (queuedFollowUps.length > 0) {
9900
+ return runProxyCommandWithQueuedFollowUps(selectedBin, finalArgs, queuedFollowUps);
9901
+ }
9902
+
9262
9903
  return new Promise((resolve, reject) => {
9263
9904
  const child = spawn(selectedBin, finalArgs, {
9264
9905
  stdio: 'inherit',
@@ -9288,17 +9929,10 @@ async function runProxyCommand(displayName, binNames, args = [], installTip = ''
9288
9929
  }
9289
9930
 
9290
9931
  async function cmdCodex(args = []) {
9291
- const ensureResult = await ensureBuiltinProxyForCodexDefault({});
9292
- if (!ensureResult || ensureResult.success !== true) {
9293
- const message = ensureResult && ensureResult.error
9294
- ? ensureResult.error
9295
- : '内建代理准备失败';
9296
- throw new Error(message);
9297
- }
9298
- if (ensureResult.runtime && ensureResult.runtime.listenUrl) {
9299
- console.log(`~ Codex 默认走内建代理: ${ensureResult.runtime.listenUrl}`);
9300
- }
9301
- return runProxyCommand('Codex', 'codex', args);
9932
+ const parsed = parseCodexProxyOptions(args);
9933
+ return runProxyCommand('Codex', 'codex', parsed.passthroughArgs, '', {
9934
+ queuedFollowUps: parsed.queuedFollowUps
9935
+ });
9302
9936
  }
9303
9937
 
9304
9938
  async function cmdQwen(args = []) {
@@ -10765,12 +11399,14 @@ async function main() {
10765
11399
  console.log(' codexmate proxy <status|set|apply|enable|start|stop> 内建代理');
10766
11400
  console.log(' codexmate workflow <list|get|validate|run|runs> MCP 工作流中心');
10767
11401
  console.log(' codexmate run [--host <HOST>] [--no-browser] 启动 Web 界面');
10768
- console.log(' codexmate codex [参数...] 等同于 codex --yolo');
11402
+ console.log(' codexmate codex [参数...] [--follow-up <文本>|--queued-follow-up <文本> 可重复] 等同于 codex --yolo(不会自动启用内建代理)');
11403
+ console.log(' 注: follow-up 自动排队仅支持 linux/android/netbsd/openbsd/darwin/freebsd 且 stdin 必须是 TTY,其他平台会报错');
10769
11404
  console.log(' codexmate qwen [参数...] 等同于 qwen --yolo');
10770
11405
  console.log(' codexmate mcp [serve] [--transport stdio] [--allow-write|--read-only]');
10771
11406
  console.log(' codexmate export-session --source <codex|claude> (--session-id <ID>|--file <PATH>) [--output <PATH>] [--max-messages <N|all|Infinity>]');
10772
11407
  console.log(' codexmate zip <路径> [--max:级别] 压缩(系统 zip 优先,其次 zip-lib)');
10773
11408
  console.log(' codexmate unzip <zip文件> [输出目录] 解压(zip-lib)');
11409
+ console.log(' codexmate unzip-ext <zip目录> [输出目录] [--ext:后缀[,后缀...]] [--no-recursive] 批量提取 ZIP 指定后缀文件(默认递归)');
10774
11410
  console.log('');
10775
11411
  process.exit(0);
10776
11412
  }
@@ -10823,6 +11459,38 @@ async function main() {
10823
11459
  break;
10824
11460
  }
10825
11461
  case 'unzip': await cmdUnzip(args[1], args[2]); break;
11462
+ case 'unzip-ext': {
11463
+ const unzipExtOptions = {
11464
+ ext: [],
11465
+ recursive: true
11466
+ };
11467
+ let zipDirPath = null;
11468
+ let outputDir = null;
11469
+ for (let i = 1; i < args.length; i++) {
11470
+ const arg = args[i];
11471
+ if (arg.startsWith('--ext:')) {
11472
+ unzipExtOptions.ext.push(...splitExtractSuffixInput(arg.substring(6)));
11473
+ } else if (arg.startsWith('--ext=')) {
11474
+ unzipExtOptions.ext.push(...splitExtractSuffixInput(arg.substring(6)));
11475
+ } else if (arg === '--ext') {
11476
+ const nextArg = args[i + 1];
11477
+ if (typeof nextArg === 'string' && !nextArg.startsWith('--')) {
11478
+ unzipExtOptions.ext.push(...splitExtractSuffixInput(nextArg));
11479
+ i += 1;
11480
+ }
11481
+ } else if (arg === '--recursive') {
11482
+ unzipExtOptions.recursive = true;
11483
+ } else if (arg === '--no-recursive') {
11484
+ unzipExtOptions.recursive = false;
11485
+ } else if (!zipDirPath) {
11486
+ zipDirPath = arg;
11487
+ } else if (!outputDir) {
11488
+ outputDir = arg;
11489
+ }
11490
+ }
11491
+ await cmdUnzipExt(zipDirPath, outputDir, unzipExtOptions);
11492
+ break;
11493
+ }
10826
11494
  default:
10827
11495
  console.error('错误: 未知命令:', command);
10828
11496
  console.log('运行 "codexmate" 查看帮助');