codexmate 0.0.15 → 0.0.16

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.en.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  **Local configuration and session manager for Codex / Claude Code / OpenClaw**
6
6
 
7
- > Current version: `v0.0.15`
7
+ > Current version: `v0.0.16`
8
8
 
9
9
  [![Build](https://img.shields.io/github/actions/workflow/status/SakuraByteCore/codexmate/release.yml?label=build)](https://github.com/SakuraByteCore/codexmate/actions/workflows/release.yml)
10
10
  [![Version](https://img.shields.io/npm/v/codexmate?label=version&registry_uri=https%3A%2F%2Fregistry.npmjs.org)](https://www.npmjs.com/package/codexmate)
@@ -130,11 +130,22 @@ codexmate run --no-browser
130
130
  | `codexmate auth <list\|import\|switch\|delete\|status>` | Auth profile management |
131
131
  | `codexmate proxy <status\|set\|apply\|enable\|start\|stop>` | Built-in proxy management |
132
132
  | `codexmate workflow <list\|get\|validate\|run\|runs>` | MCP workflow management |
133
+ | `codexmate codex [args...] [--follow-up <text> repeatable]` | Codex CLI passthrough entrypoint (auto-adds `--yolo`, supports queued follow-up appends) |
133
134
  | `codexmate qwen [args...]` | Qwen CLI passthrough entrypoint |
134
135
  | `codexmate run [--host <HOST>] [--no-browser]` | Start Web UI |
135
136
  | `codexmate mcp serve [--read-only\|--allow-write]` | Start MCP stdio server |
136
137
  | `codexmate export-session --source <codex\|claude> ...` | Export session to Markdown |
137
138
  | `codexmate zip <path> [--max:0-9]` / `codexmate unzip <zip> [out]` | Zip / unzip |
139
+ | `codexmate unzip-ext <zip-dir> [out] [--ext:suffix[,suffix...]] [--no-recursive]` | Extract files with target suffixes from ZIP files in a directory (default `.json`, recursive by default) |
140
+
141
+ ### Codex Follow-up Append (Optional)
142
+
143
+ ```bash
144
+ codexmate codex --follow-up "scan repository first" --follow-up "then fix failing tests"
145
+ codexmate codex --model gpt-5.3-codex --follow-up "step1" --follow-up "step2"
146
+ ```
147
+
148
+ > Note: both `--follow-up` and `--queued-follow-up` are accepted and repeatable.
138
149
 
139
150
  ## Web UI
140
151
 
package/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  **Codex / Claude Code / OpenClaw 的本地配置与会话管理工具**
6
6
 
7
- > 当前版本:`v0.0.15`
7
+ > 当前版本:`v0.0.16`
8
8
 
9
9
  [![Build](https://img.shields.io/github/actions/workflow/status/SakuraByteCore/codexmate/release.yml?label=build)](https://github.com/SakuraByteCore/codexmate/actions/workflows/release.yml)
10
10
  [![Version](https://img.shields.io/npm/v/codexmate?label=version&registry_uri=https%3A%2F%2Fregistry.npmjs.org)](https://www.npmjs.com/package/codexmate)
@@ -130,11 +130,22 @@ codexmate run --no-browser
130
130
  | `codexmate auth <list\|import\|switch\|delete\|status>` | 认证档案管理 |
131
131
  | `codexmate proxy <status\|set\|apply\|enable\|start\|stop>` | 内建代理管理 |
132
132
  | `codexmate workflow <list\|get\|validate\|run\|runs>` | MCP 工作流管理 |
133
+ | `codexmate codex [args...] [--follow-up <文本> 可重复]` | Codex CLI 透传入口(默认补 `--yolo`,可追加 queued follow-up) |
133
134
  | `codexmate qwen [args...]` | Qwen CLI 透传入口 |
134
135
  | `codexmate run [--host <HOST>] [--no-browser]` | 启动 Web UI |
135
136
  | `codexmate mcp serve [--read-only\|--allow-write]` | 启动 MCP stdio 服务 |
136
137
  | `codexmate export-session --source <codex\|claude> ...` | 导出会话为 Markdown |
137
138
  | `codexmate zip <path> [--max:0-9]` / `codexmate unzip <zip> [out]` | 压缩 / 解压 |
139
+ | `codexmate unzip-ext <zip-dir> [out] [--ext:suffix[,suffix...]] [--no-recursive]` | 批量提取目录下 ZIP 内指定后缀文件(默认 `.json`,默认递归) |
140
+
141
+ ### Codex follow-up 追加(可选)
142
+
143
+ ```bash
144
+ codexmate codex --follow-up "先扫描项目" --follow-up "再修复失败测试"
145
+ codexmate codex --model gpt-5.3-codex --follow-up "步骤1" --follow-up "步骤2"
146
+ ```
147
+
148
+ > 说明:`--follow-up` / `--queued-follow-up` 都可用,支持重复。
138
149
 
139
150
  ## Web 界面
140
151
 
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';
@@ -7784,6 +7785,318 @@ async function cmdUnzip(zipPath, outputDir) {
7784
7785
  }
7785
7786
  }
7786
7787
 
7788
+ function splitExtractSuffixInput(rawValue) {
7789
+ if (Array.isArray(rawValue)) {
7790
+ return rawValue.flatMap((item) => splitExtractSuffixInput(item));
7791
+ }
7792
+ if (typeof rawValue !== 'string') {
7793
+ return [];
7794
+ }
7795
+ return rawValue
7796
+ .split(/[,\s]+/g)
7797
+ .map((item) => item.trim())
7798
+ .filter(Boolean);
7799
+ }
7800
+
7801
+ function normalizeExtractSuffix(rawSuffix, fallbackSuffixes = DEFAULT_EXTRACT_SUFFIXES) {
7802
+ const fallbackItems = splitExtractSuffixInput(fallbackSuffixes);
7803
+ const sourceItems = splitExtractSuffixInput(rawSuffix);
7804
+ const source = sourceItems.length > 0 ? sourceItems : fallbackItems;
7805
+ const dedup = new Set();
7806
+
7807
+ for (const item of source) {
7808
+ const lower = item.toLowerCase();
7809
+ if (!lower) {
7810
+ continue;
7811
+ }
7812
+ const normalized = lower.startsWith('.') ? lower : `.${lower}`;
7813
+ if (normalized.length > 1) {
7814
+ dedup.add(normalized);
7815
+ }
7816
+ }
7817
+
7818
+ if (dedup.size === 0) {
7819
+ return [...DEFAULT_EXTRACT_SUFFIXES];
7820
+ }
7821
+ return Array.from(dedup);
7822
+ }
7823
+
7824
+ function buildDefaultExtractOutputDir(baseCwd = process.cwd()) {
7825
+ const normalizedCwd = path.resolve(baseCwd);
7826
+ const parentDir = path.dirname(normalizedCwd);
7827
+ const timestamp = formatTimestampForFileName().replace(/-/g, '');
7828
+ return path.join(parentDir, timestamp);
7829
+ }
7830
+
7831
+ function sanitizeNameSegment(rawValue, fallback = 'item') {
7832
+ const value = typeof rawValue === 'string' ? rawValue.trim() : '';
7833
+ const sanitized = value
7834
+ .replace(/[^\w.-]+/g, '_')
7835
+ .replace(/^_+|_+$/g, '');
7836
+ return sanitized || fallback;
7837
+ }
7838
+
7839
+ function resolveDuplicateOutputPath(outputDir, originalFileName, zipPath = '', counters = new Map()) {
7840
+ const fallbackName = `file${path.extname(originalFileName || '')}`;
7841
+ const fileName = path.basename(originalFileName || '') || fallbackName;
7842
+ const firstChoice = path.join(outputDir, fileName);
7843
+ const firstChoiceKey = `exact:${fileName}`;
7844
+ if (!counters.has(firstChoiceKey)) {
7845
+ counters.set(firstChoiceKey, true);
7846
+ if (!fs.existsSync(firstChoice)) {
7847
+ return firstChoice;
7848
+ }
7849
+ }
7850
+
7851
+ const ext = path.extname(fileName);
7852
+ const baseName = path.basename(fileName, ext);
7853
+ const safeBaseName = sanitizeNameSegment(baseName, 'file');
7854
+ const zipBaseName = sanitizeNameSegment(path.basename(zipPath || '', '.zip'), 'zip');
7855
+ const duplicateKey = `dup:${safeBaseName}|${zipBaseName}|${ext}`;
7856
+ let index = counters.has(duplicateKey) ? counters.get(duplicateKey) : 1;
7857
+
7858
+ for (; index <= 100000; index++) {
7859
+ const candidateName = `${safeBaseName}__${zipBaseName}__${index}${ext}`;
7860
+ const candidatePath = path.join(outputDir, candidateName);
7861
+ if (!fs.existsSync(candidatePath)) {
7862
+ counters.set(duplicateKey, index + 1);
7863
+ return candidatePath;
7864
+ }
7865
+ }
7866
+
7867
+ throw new Error(`重名文件过多,无法生成唯一文件名: ${fileName}`);
7868
+ }
7869
+
7870
+ function collectZipFilesFromDir(rootDir, recursive = true) {
7871
+ const queue = [rootDir];
7872
+ const result = [];
7873
+
7874
+ while (queue.length > 0) {
7875
+ const currentDir = queue.shift();
7876
+ let entries = [];
7877
+ try {
7878
+ entries = fs.readdirSync(currentDir, { withFileTypes: true });
7879
+ } catch (e) {
7880
+ throw new Error(`读取目录失败: ${currentDir} (${e.message})`);
7881
+ }
7882
+
7883
+ for (const entry of entries) {
7884
+ const entryPath = path.join(currentDir, entry.name);
7885
+ if (entry.isDirectory()) {
7886
+ if (recursive) {
7887
+ queue.push(entryPath);
7888
+ }
7889
+ continue;
7890
+ }
7891
+ if (entry.isFile() && entry.name.toLowerCase().endsWith('.zip')) {
7892
+ result.push(entryPath);
7893
+ }
7894
+ }
7895
+ }
7896
+
7897
+ result.sort((a, b) => a.localeCompare(b));
7898
+ return result;
7899
+ }
7900
+
7901
+ function extractMatchedEntriesFromZip(zipPath, outputDir, suffixes, duplicateCounters = new Map()) {
7902
+ const normalizedSuffixes = normalizeExtractSuffix(suffixes);
7903
+ return new Promise((resolve, reject) => {
7904
+ yauzl.open(zipPath, { lazyEntries: true, autoClose: false }, (openErr, zipFile) => {
7905
+ if (openErr) {
7906
+ reject(openErr);
7907
+ return;
7908
+ }
7909
+ if (!zipFile) {
7910
+ reject(new Error('无法读取 ZIP 文件'));
7911
+ return;
7912
+ }
7913
+
7914
+ let settled = false;
7915
+ let matched = 0;
7916
+ let extracted = 0;
7917
+ let skippedDir = 0;
7918
+ let skippedExt = 0;
7919
+
7920
+ const finish = (err) => {
7921
+ if (settled) return;
7922
+ settled = true;
7923
+ try {
7924
+ zipFile.close();
7925
+ } catch (_) {}
7926
+ if (err) {
7927
+ reject(err);
7928
+ } else {
7929
+ resolve({ matched, extracted, skippedDir, skippedExt });
7930
+ }
7931
+ };
7932
+
7933
+ zipFile.on('entry', (entry) => {
7934
+ if (settled) return;
7935
+ const rawEntryName = typeof entry.fileName === 'string' ? entry.fileName : '';
7936
+ const normalizedEntryName = rawEntryName.replace(/\\/g, '/');
7937
+
7938
+ if (!normalizedEntryName || normalizedEntryName.endsWith('/')) {
7939
+ skippedDir += 1;
7940
+ zipFile.readEntry();
7941
+ return;
7942
+ }
7943
+
7944
+ const entryBaseName = path.basename(normalizedEntryName);
7945
+ const lowerBaseName = entryBaseName.toLowerCase();
7946
+ const matchedSuffix = normalizedSuffixes.some((suffix) => lowerBaseName.endsWith(suffix));
7947
+ if (!entryBaseName || !matchedSuffix) {
7948
+ skippedExt += 1;
7949
+ zipFile.readEntry();
7950
+ return;
7951
+ }
7952
+
7953
+ matched += 1;
7954
+ zipFile.openReadStream(entry, (streamErr, readStream) => {
7955
+ if (streamErr || !readStream) {
7956
+ finish(streamErr || new Error('无法读取 ZIP 条目流'));
7957
+ return;
7958
+ }
7959
+
7960
+ let completed = false;
7961
+ const outputPath = resolveDuplicateOutputPath(outputDir, entryBaseName, zipPath, duplicateCounters);
7962
+ const writeStream = fs.createWriteStream(outputPath);
7963
+ const fail = (writeErr) => {
7964
+ if (completed) return;
7965
+ completed = true;
7966
+ try {
7967
+ readStream.destroy();
7968
+ } catch (_) {}
7969
+ try {
7970
+ writeStream.destroy();
7971
+ } catch (_) {}
7972
+ try {
7973
+ if (fs.existsSync(outputPath)) {
7974
+ fs.unlinkSync(outputPath);
7975
+ }
7976
+ } catch (_) {}
7977
+ finish(writeErr);
7978
+ };
7979
+
7980
+ readStream.on('error', fail);
7981
+ writeStream.on('error', fail);
7982
+ writeStream.on('finish', () => {
7983
+ if (completed || settled) return;
7984
+ completed = true;
7985
+ extracted += 1;
7986
+ zipFile.readEntry();
7987
+ });
7988
+
7989
+ readStream.pipe(writeStream);
7990
+ });
7991
+ });
7992
+
7993
+ zipFile.on('end', () => {
7994
+ finish(null);
7995
+ });
7996
+ zipFile.on('error', (zipErr) => {
7997
+ finish(zipErr);
7998
+ });
7999
+
8000
+ zipFile.readEntry();
8001
+ });
8002
+ });
8003
+ }
8004
+
8005
+ async function cmdUnzipExt(zipDirPath, outputDir, options = {}) {
8006
+ if (!zipDirPath) {
8007
+ console.error('用法: codexmate unzip-ext <zip目录> [输出目录] [--ext:后缀[,后缀...]] [--no-recursive]');
8008
+ console.log('\n示例:');
8009
+ console.log(' codexmate unzip-ext ./archives');
8010
+ console.log(' codexmate unzip-ext ./archives ./output --ext:json,txt');
8011
+ console.log(' codexmate unzip-ext D:/data/zips --ext:txt --no-recursive');
8012
+ console.log(' 说明: 默认递归扫描子目录,可通过 --no-recursive 关闭递归');
8013
+ process.exit(1);
8014
+ }
8015
+
8016
+ const recursive = options.recursive !== false;
8017
+ const suffixes = normalizeExtractSuffix(options.ext);
8018
+ const absZipDir = path.resolve(zipDirPath);
8019
+ const absOutputDir = outputDir ? path.resolve(outputDir) : buildDefaultExtractOutputDir(process.cwd());
8020
+
8021
+ if (!fs.existsSync(absZipDir)) {
8022
+ console.error('错误: 目录不存在:', absZipDir);
8023
+ process.exit(1);
8024
+ }
8025
+ try {
8026
+ if (!fs.statSync(absZipDir).isDirectory()) {
8027
+ console.error('错误: 仅支持目录路径:', absZipDir);
8028
+ process.exit(1);
8029
+ }
8030
+ } catch (e) {
8031
+ console.error('错误: 无法读取目录信息:', e.message);
8032
+ process.exit(1);
8033
+ }
8034
+
8035
+ let zipFiles = [];
8036
+ try {
8037
+ zipFiles = collectZipFilesFromDir(absZipDir, recursive);
8038
+ } catch (e) {
8039
+ console.error('扫描 ZIP 文件失败:', e.message);
8040
+ process.exit(1);
8041
+ }
8042
+
8043
+ if (zipFiles.length === 0) {
8044
+ console.error('错误: 未找到任何 ZIP 文件');
8045
+ process.exit(1);
8046
+ }
8047
+
8048
+ ensureDir(absOutputDir);
8049
+
8050
+ console.log('\n批量解压配置:');
8051
+ console.log(' ZIP 目录:', absZipDir);
8052
+ console.log(' 输出目录:', absOutputDir);
8053
+ console.log(' 后缀过滤:', suffixes.join(', '));
8054
+ console.log(' 递归扫描:', recursive ? '是' : '否');
8055
+ console.log(' ZIP 数量:', zipFiles.length);
8056
+ console.log('\n开始提取...\n');
8057
+
8058
+ let totalMatched = 0;
8059
+ let totalExtracted = 0;
8060
+ let totalSkippedDir = 0;
8061
+ let totalSkippedExt = 0;
8062
+ const failed = [];
8063
+ const duplicateCounters = new Map();
8064
+
8065
+ for (const zipFilePath of zipFiles) {
8066
+ try {
8067
+ await inspectZipArchiveLimits(zipFilePath, {
8068
+ maxEntryCount: MAX_SKILLS_ZIP_ENTRY_COUNT,
8069
+ maxUncompressedBytes: MAX_SKILLS_ZIP_UNCOMPRESSED_BYTES
8070
+ });
8071
+ const result = await extractMatchedEntriesFromZip(zipFilePath, absOutputDir, suffixes, duplicateCounters);
8072
+ totalMatched += result.matched;
8073
+ totalExtracted += result.extracted;
8074
+ totalSkippedDir += result.skippedDir;
8075
+ totalSkippedExt += result.skippedExt;
8076
+ console.log(`✓ ${path.basename(zipFilePath)}: 命中 ${result.matched},提取 ${result.extracted}`);
8077
+ } catch (e) {
8078
+ failed.push({ zipFilePath, message: e && e.message ? e.message : String(e) });
8079
+ console.error(`✗ ${path.basename(zipFilePath)}: ${e && e.message ? e.message : e}`);
8080
+ }
8081
+ }
8082
+
8083
+ console.log('\n提取结果:');
8084
+ console.log(' 输出目录:', absOutputDir);
8085
+ console.log(' 扫描 ZIP:', zipFiles.length);
8086
+ console.log(' 命中条目:', totalMatched);
8087
+ console.log(' 已提取:', totalExtracted);
8088
+ console.log(' 已跳过(目录条目):', totalSkippedDir);
8089
+ console.log(' 已跳过(后缀不匹配):', totalSkippedExt);
8090
+ if (failed.length > 0) {
8091
+ console.error(' 失败数量:', failed.length);
8092
+ for (const item of failed) {
8093
+ console.error(` - ${item.zipFilePath}: ${item.message}`);
8094
+ }
8095
+ process.exit(1);
8096
+ }
8097
+ console.log();
8098
+ }
8099
+
7787
8100
  function resolveExportOutputPath(outputPath, defaultFileName) {
7788
8101
  const fallback = path.resolve(process.cwd(), defaultFileName);
7789
8102
  if (typeof outputPath !== 'string' || !outputPath.trim()) {
@@ -9233,7 +9546,317 @@ async function cmdWorkflow(args = []) {
9233
9546
  throw new Error(`未知 workflow 子命令: ${subcommand}`);
9234
9547
  }
9235
9548
 
9236
- async function runProxyCommand(displayName, binNames, args = [], installTip = '') {
9549
+ // #region parseCodexProxyOptions
9550
+ function parseCodexProxyOptions(args = []) {
9551
+ const options = {
9552
+ passthroughArgs: [],
9553
+ queuedFollowUps: []
9554
+ };
9555
+ const argv = Array.isArray(args) ? args : [];
9556
+
9557
+ const pushFollowUp = (value, optionName) => {
9558
+ const raw = value === undefined || value === null ? '' : String(value);
9559
+ if (!raw.trim()) {
9560
+ throw new Error(`${optionName} 需要提供非空内容`);
9561
+ }
9562
+ options.queuedFollowUps.push(raw);
9563
+ };
9564
+
9565
+ for (let i = 0; i < argv.length; i++) {
9566
+ const arg = argv[i];
9567
+ if (arg === undefined || arg === null) {
9568
+ continue;
9569
+ }
9570
+ const text = String(arg);
9571
+ if (text === '--') {
9572
+ options.passthroughArgs.push(...argv.slice(i).map((item) => String(item)));
9573
+ break;
9574
+ }
9575
+ if (text === '--queued-follow-up' || text === '--follow-up') {
9576
+ const next = argv[i + 1];
9577
+ if (next === undefined) {
9578
+ throw new Error(`${text} 需要提供内容`);
9579
+ }
9580
+ pushFollowUp(next, text);
9581
+ i += 1;
9582
+ continue;
9583
+ }
9584
+ if (text.startsWith('--queued-follow-up=')) {
9585
+ pushFollowUp(text.slice('--queued-follow-up='.length), '--queued-follow-up');
9586
+ continue;
9587
+ }
9588
+ if (text.startsWith('--follow-up=')) {
9589
+ pushFollowUp(text.slice('--follow-up='.length), '--follow-up');
9590
+ continue;
9591
+ }
9592
+ options.passthroughArgs.push(text);
9593
+ }
9594
+
9595
+ return options;
9596
+ }
9597
+ // #endregion parseCodexProxyOptions
9598
+
9599
+ function shellEscapePosixArg(value) {
9600
+ const text = value === undefined || value === null ? '' : String(value);
9601
+ return `'${text.replace(/'/g, `'\"'\"'`)}'`;
9602
+ }
9603
+
9604
+ // #region buildScriptCommandArgs
9605
+ function buildScriptCommandArgs(commandLine) {
9606
+ const platform = process.platform;
9607
+ // util-linux script needs -e/--return to propagate child exit code.
9608
+ if (platform === 'linux' || platform === 'android') {
9609
+ return ['-q', '-e', '-c', commandLine, '/dev/null'];
9610
+ }
9611
+ // NetBSD supports -e/-c, matching util-linux style contract.
9612
+ if (platform === 'netbsd') {
9613
+ return ['-q', '-e', '-c', commandLine, '/dev/null'];
9614
+ }
9615
+ // OpenBSD supports "-c <command>" with a trailing output file path.
9616
+ if (platform === 'openbsd') {
9617
+ return ['-c', commandLine, '/dev/null'];
9618
+ }
9619
+ // BSD/macOS script does not support util-linux "-c <cmd>" syntax.
9620
+ if (platform === 'darwin' || platform === 'freebsd') {
9621
+ return ['-q', '/dev/null', 'sh', '-lc', commandLine];
9622
+ }
9623
+ throw new Error(`当前平台暂不支持 --follow-up 自动排队(platform=${platform})`);
9624
+ }
9625
+ // #endregion buildScriptCommandArgs
9626
+
9627
+ // #region runProxyCommandWithQueuedFollowUps
9628
+ async function runProxyCommandWithQueuedFollowUps(selectedBin, finalArgs = [], queuedFollowUps = []) {
9629
+ if (!process.stdin || !process.stdin.isTTY) {
9630
+ throw new Error('当前 stdin 不是 TTY,无法使用 --follow-up 自动排队。');
9631
+ }
9632
+
9633
+ const scriptPath = resolveCommandPath('script');
9634
+ if (!scriptPath) {
9635
+ throw new Error('未找到 script 命令,无法自动注入 queued follow-up 消息。');
9636
+ }
9637
+
9638
+ const commandLine = [selectedBin, ...finalArgs].map((item) => shellEscapePosixArg(item)).join(' ');
9639
+ const scriptArgs = buildScriptCommandArgs(commandLine);
9640
+
9641
+ return new Promise((resolve, reject) => {
9642
+ let settled = false;
9643
+ const child = spawn(scriptPath, scriptArgs, {
9644
+ stdio: ['pipe', 'pipe', 'pipe']
9645
+ });
9646
+
9647
+ const stdin = process.stdin;
9648
+ const hadRawMode = !!stdin.isRaw;
9649
+ let cleanedUp = false;
9650
+ let waitingDrain = false;
9651
+ let followUpsFlushed = false;
9652
+ let outputReadyDetected = false;
9653
+ const timers = [];
9654
+ const pendingWrites = [];
9655
+ let onChildStdinDrain = null;
9656
+ let onChildStdinError = null;
9657
+ const resolveOnce = (code) => {
9658
+ if (settled) return;
9659
+ settled = true;
9660
+ resolve(code);
9661
+ };
9662
+ const rejectOnce = (error) => {
9663
+ if (settled) return;
9664
+ settled = true;
9665
+ reject(error);
9666
+ };
9667
+ const handleWriteFailure = (error) => {
9668
+ const err = error instanceof Error ? error : new Error(String(error || 'unknown'));
9669
+ cleanup();
9670
+ try {
9671
+ if (!child.killed) {
9672
+ child.kill('SIGTERM');
9673
+ }
9674
+ } catch (_) {
9675
+ // Ignore failure to terminate child after stdin write failure.
9676
+ }
9677
+ rejectOnce(new Error(`写入 ${selectedBin} stdin 失败: ${err.message}`));
9678
+ };
9679
+ const flushPendingWrites = () => {
9680
+ if (cleanedUp || child.stdin.destroyed) {
9681
+ pendingWrites.length = 0;
9682
+ return;
9683
+ }
9684
+ while (pendingWrites.length > 0) {
9685
+ const chunk = pendingWrites[0];
9686
+ let canContinue = true;
9687
+ try {
9688
+ canContinue = child.stdin.write(chunk, (error) => {
9689
+ if (error) {
9690
+ handleWriteFailure(error);
9691
+ }
9692
+ });
9693
+ } catch (error) {
9694
+ handleWriteFailure(error);
9695
+ return;
9696
+ }
9697
+ pendingWrites.shift();
9698
+ if (!canContinue) {
9699
+ waitingDrain = true;
9700
+ try {
9701
+ stdin.pause();
9702
+ } catch (_) {
9703
+ // Ignore stdin pause failures.
9704
+ }
9705
+ return;
9706
+ }
9707
+ }
9708
+ waitingDrain = false;
9709
+ try {
9710
+ stdin.resume();
9711
+ } catch (_) {
9712
+ // Ignore stdin resume failures.
9713
+ }
9714
+ };
9715
+ const enqueueWrite = (chunk) => {
9716
+ if (cleanedUp) return;
9717
+ pendingWrites.push(chunk);
9718
+ flushPendingWrites();
9719
+ };
9720
+ const onInput = (chunk) => {
9721
+ if (!child.stdin.destroyed) {
9722
+ enqueueWrite(chunk);
9723
+ }
9724
+ };
9725
+ const flushQueuedFollowUps = () => {
9726
+ if (followUpsFlushed) return;
9727
+ followUpsFlushed = true;
9728
+ queuedFollowUps.forEach((message, index) => {
9729
+ const timer = setTimeout(() => {
9730
+ if (!child.stdin.destroyed) {
9731
+ // PTY submit should use CR instead of LF.
9732
+ enqueueWrite(`${message}\r`);
9733
+ }
9734
+ }, index * 80);
9735
+ timers.push(timer);
9736
+ });
9737
+ };
9738
+ const markOutputReady = () => {
9739
+ if (outputReadyDetected) return;
9740
+ outputReadyDetected = true;
9741
+ timers.push(setTimeout(() => {
9742
+ flushQueuedFollowUps();
9743
+ }, 120));
9744
+ };
9745
+ const onStdoutData = (chunk) => {
9746
+ process.stdout.write(chunk);
9747
+ markOutputReady();
9748
+ };
9749
+ const onStderrData = (chunk) => {
9750
+ process.stderr.write(chunk);
9751
+ markOutputReady();
9752
+ };
9753
+ const onProcessExit = () => {
9754
+ cleanup();
9755
+ };
9756
+ const onProcessSigint = () => {
9757
+ cleanup();
9758
+ try {
9759
+ if (!child.killed) {
9760
+ child.kill('SIGINT');
9761
+ }
9762
+ } catch (_) {
9763
+ // Ignore forwarding failures and keep exit path deterministic.
9764
+ }
9765
+ process.exit(130);
9766
+ };
9767
+ const onProcessSigterm = () => {
9768
+ cleanup();
9769
+ try {
9770
+ if (!child.killed) {
9771
+ child.kill('SIGTERM');
9772
+ }
9773
+ } catch (_) {
9774
+ // Ignore forwarding failures and keep exit path deterministic.
9775
+ }
9776
+ process.exit(143);
9777
+ };
9778
+ const cleanup = () => {
9779
+ if (cleanedUp) return;
9780
+ cleanedUp = true;
9781
+ stdin.removeListener('data', onInput);
9782
+ process.removeListener('exit', onProcessExit);
9783
+ process.removeListener('SIGINT', onProcessSigint);
9784
+ process.removeListener('SIGTERM', onProcessSigterm);
9785
+ child.stdout.removeListener('data', onStdoutData);
9786
+ child.stderr.removeListener('data', onStderrData);
9787
+ if (onChildStdinDrain) {
9788
+ child.stdin.removeListener('drain', onChildStdinDrain);
9789
+ }
9790
+ if (onChildStdinError) {
9791
+ child.stdin.removeListener('error', onChildStdinError);
9792
+ }
9793
+ while (timers.length > 0) {
9794
+ clearTimeout(timers.pop());
9795
+ }
9796
+ try {
9797
+ if (typeof stdin.setRawMode === 'function' && !hadRawMode) {
9798
+ stdin.setRawMode(false);
9799
+ }
9800
+ } catch (_) {
9801
+ // Ignore raw mode restore failures at shutdown.
9802
+ }
9803
+ };
9804
+
9805
+ process.on('exit', onProcessExit);
9806
+ process.on('SIGINT', onProcessSigint);
9807
+ process.on('SIGTERM', onProcessSigterm);
9808
+ child.stdout.on('data', onStdoutData);
9809
+ child.stderr.on('data', onStderrData);
9810
+ onChildStdinDrain = () => {
9811
+ waitingDrain = false;
9812
+ flushPendingWrites();
9813
+ };
9814
+ onChildStdinError = (error) => {
9815
+ handleWriteFailure(error);
9816
+ };
9817
+ child.stdin.on('drain', onChildStdinDrain);
9818
+ child.stdin.on('error', onChildStdinError);
9819
+ try {
9820
+ if (typeof stdin.setRawMode === 'function' && !hadRawMode) {
9821
+ stdin.setRawMode(true);
9822
+ }
9823
+ } catch (_) {
9824
+ // Keep graceful fallback if raw mode toggle is not supported.
9825
+ }
9826
+
9827
+ stdin.resume();
9828
+ stdin.on('data', onInput);
9829
+ // Fallback in case the child stays silent before prompt render.
9830
+ timers.push(setTimeout(() => {
9831
+ flushQueuedFollowUps();
9832
+ }, 1500));
9833
+
9834
+ child.on('error', (err) => {
9835
+ cleanup();
9836
+ rejectOnce(new Error(`运行 ${selectedBin} 失败: ${err.message}`));
9837
+ });
9838
+
9839
+ child.on('close', (code, signal) => {
9840
+ cleanup();
9841
+ if (typeof code === 'number') {
9842
+ resolveOnce(code);
9843
+ return;
9844
+ }
9845
+ if (signal === 'SIGINT') {
9846
+ resolveOnce(130);
9847
+ return;
9848
+ }
9849
+ if (signal === 'SIGTERM') {
9850
+ resolveOnce(143);
9851
+ return;
9852
+ }
9853
+ resolveOnce(1);
9854
+ });
9855
+ });
9856
+ }
9857
+ // #endregion runProxyCommandWithQueuedFollowUps
9858
+
9859
+ async function runProxyCommand(displayName, binNames, args = [], installTip = '', runtimeOptions = {}) {
9237
9860
  const extraArgs = Array.isArray(args) ? args.filter(arg => arg !== undefined) : [];
9238
9861
  const hasYolo = extraArgs.includes('--yolo');
9239
9862
  const finalArgs = hasYolo ? extraArgs : ['--yolo', ...extraArgs];
@@ -9259,6 +9882,14 @@ async function runProxyCommand(displayName, binNames, args = [], installTip = ''
9259
9882
  throw new Error(msg);
9260
9883
  }
9261
9884
 
9885
+ const queuedFollowUps = runtimeOptions && Array.isArray(runtimeOptions.queuedFollowUps)
9886
+ ? runtimeOptions.queuedFollowUps.filter((item) => typeof item === 'string' && item.trim())
9887
+ : [];
9888
+
9889
+ if (queuedFollowUps.length > 0) {
9890
+ return runProxyCommandWithQueuedFollowUps(selectedBin, finalArgs, queuedFollowUps);
9891
+ }
9892
+
9262
9893
  return new Promise((resolve, reject) => {
9263
9894
  const child = spawn(selectedBin, finalArgs, {
9264
9895
  stdio: 'inherit',
@@ -9288,17 +9919,10 @@ async function runProxyCommand(displayName, binNames, args = [], installTip = ''
9288
9919
  }
9289
9920
 
9290
9921
  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);
9922
+ const parsed = parseCodexProxyOptions(args);
9923
+ return runProxyCommand('Codex', 'codex', parsed.passthroughArgs, '', {
9924
+ queuedFollowUps: parsed.queuedFollowUps
9925
+ });
9302
9926
  }
9303
9927
 
9304
9928
  async function cmdQwen(args = []) {
@@ -10765,12 +11389,14 @@ async function main() {
10765
11389
  console.log(' codexmate proxy <status|set|apply|enable|start|stop> 内建代理');
10766
11390
  console.log(' codexmate workflow <list|get|validate|run|runs> MCP 工作流中心');
10767
11391
  console.log(' codexmate run [--host <HOST>] [--no-browser] 启动 Web 界面');
10768
- console.log(' codexmate codex [参数...] 等同于 codex --yolo');
11392
+ console.log(' codexmate codex [参数...] [--follow-up <文本>|--queued-follow-up <文本> 可重复] 等同于 codex --yolo(不会自动启用内建代理)');
11393
+ console.log(' 注: follow-up 自动排队仅支持 linux/android/netbsd/openbsd/darwin/freebsd 且 stdin 必须是 TTY,其他平台会报错');
10769
11394
  console.log(' codexmate qwen [参数...] 等同于 qwen --yolo');
10770
11395
  console.log(' codexmate mcp [serve] [--transport stdio] [--allow-write|--read-only]');
10771
11396
  console.log(' codexmate export-session --source <codex|claude> (--session-id <ID>|--file <PATH>) [--output <PATH>] [--max-messages <N|all|Infinity>]');
10772
11397
  console.log(' codexmate zip <路径> [--max:级别] 压缩(系统 zip 优先,其次 zip-lib)');
10773
11398
  console.log(' codexmate unzip <zip文件> [输出目录] 解压(zip-lib)');
11399
+ console.log(' codexmate unzip-ext <zip目录> [输出目录] [--ext:后缀[,后缀...]] [--no-recursive] 批量提取 ZIP 指定后缀文件(默认递归)');
10774
11400
  console.log('');
10775
11401
  process.exit(0);
10776
11402
  }
@@ -10823,6 +11449,38 @@ async function main() {
10823
11449
  break;
10824
11450
  }
10825
11451
  case 'unzip': await cmdUnzip(args[1], args[2]); break;
11452
+ case 'unzip-ext': {
11453
+ const unzipExtOptions = {
11454
+ ext: [],
11455
+ recursive: true
11456
+ };
11457
+ let zipDirPath = null;
11458
+ let outputDir = null;
11459
+ for (let i = 1; i < args.length; i++) {
11460
+ const arg = args[i];
11461
+ if (arg.startsWith('--ext:')) {
11462
+ unzipExtOptions.ext.push(...splitExtractSuffixInput(arg.substring(6)));
11463
+ } else if (arg.startsWith('--ext=')) {
11464
+ unzipExtOptions.ext.push(...splitExtractSuffixInput(arg.substring(6)));
11465
+ } else if (arg === '--ext') {
11466
+ const nextArg = args[i + 1];
11467
+ if (typeof nextArg === 'string' && !nextArg.startsWith('--')) {
11468
+ unzipExtOptions.ext.push(...splitExtractSuffixInput(nextArg));
11469
+ i += 1;
11470
+ }
11471
+ } else if (arg === '--recursive') {
11472
+ unzipExtOptions.recursive = true;
11473
+ } else if (arg === '--no-recursive') {
11474
+ unzipExtOptions.recursive = false;
11475
+ } else if (!zipDirPath) {
11476
+ zipDirPath = arg;
11477
+ } else if (!outputDir) {
11478
+ outputDir = arg;
11479
+ }
11480
+ }
11481
+ await cmdUnzipExt(zipDirPath, outputDir, unzipExtOptions);
11482
+ break;
11483
+ }
10826
11484
  default:
10827
11485
  console.error('错误: 未知命令:', command);
10828
11486
  console.log('运行 "codexmate" 查看帮助');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codexmate",
3
- "version": "0.0.15",
3
+ "version": "0.0.16",
4
4
  "description": "Codex/Claude Code 配置与会话管理 CLI + Web 工具",
5
5
  "main": "cli.js",
6
6
  "bin": {
package/web-ui/styles.css CHANGED
@@ -1,4 +1,4 @@
1
- @import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500&family=Source+Sans+3:wght@400;500;600&family=Space+Grotesk:wght@400;500;600;700&display=swap');
1
+ @import url('https://fonts.googleapis.com/css2?family=Fira+Mono:wght@400;500&family=JetBrains+Mono:wght@400;500&family=Source+Sans+3:wght@400;500;600&family=Space+Grotesk:wght@400;500;600;700&display=swap');
2
2
 
3
3
  /* ============================================
4
4
  设计系统 - Design Tokens
@@ -32,9 +32,9 @@
32
32
  linear-gradient(135deg, #F8F2EA 0%, #F1E4D8 44%, #F8F2EA 100%);
33
33
 
34
34
  /* 字体系统 */
35
- --font-family-body: 'Source Sans 3', 'Noto Sans SC', 'PingFang SC', 'Microsoft YaHei', sans-serif;
36
- --font-family-display: 'Space Grotesk', 'Noto Sans SC', 'PingFang SC', 'Microsoft YaHei', sans-serif;
37
- --font-family-mono: 'JetBrains Mono', 'SFMono-Regular', Consolas, 'Liberation Mono', monospace;
35
+ --font-family-body: 'JetBrainsMono Nerd Font Mono', 'OPPO Sans 4.0', 'Fira Mono', 'JetBrains Mono', 'Noto Sans SC', 'PingFang SC', 'Microsoft YaHei', monospace;
36
+ --font-family-display: 'JetBrainsMono Nerd Font Mono', 'OPPO Sans 4.0', 'Fira Mono', 'JetBrains Mono', 'Noto Sans SC', 'PingFang SC', 'Microsoft YaHei', monospace;
37
+ --font-family-mono: 'JetBrainsMono Nerd Font Mono', 'OPPO Sans 4.0', 'Fira Mono', 'JetBrains Mono', 'SFMono-Regular', Consolas, 'Liberation Mono', monospace;
38
38
  --font-family: var(--font-family-body);
39
39
 
40
40
  --font-size-display: 52px;