codexmate 0.0.8 → 0.0.10

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
@@ -85,9 +85,9 @@ const MAX_SESSION_DETAIL_MESSAGES = 1000;
85
85
  const SESSION_TITLE_READ_BYTES = 64 * 1024;
86
86
  const CODEXMATE_MANAGED_MARKER = '# codexmate-managed: true';
87
87
  const SESSION_LIST_CACHE_TTL_MS = 4000;
88
- const SESSION_SUMMARY_READ_BYTES = 256 * 1024;
89
- const SESSION_CONTENT_READ_BYTES = SESSION_SUMMARY_READ_BYTES;
90
- const DEFAULT_CONTENT_SCAN_LIMIT = 10;
88
+ const SESSION_SUMMARY_READ_BYTES = 256 * 1024;
89
+ const SESSION_CONTENT_READ_BYTES = SESSION_SUMMARY_READ_BYTES;
90
+ const DEFAULT_CONTENT_SCAN_LIMIT = 50;
91
91
  const SESSION_SCAN_FACTOR = 4;
92
92
  const SESSION_SCAN_MIN_FILES = 800;
93
93
  const MAX_SESSION_PATH_LIST_SIZE = 2000;
@@ -1530,17 +1530,17 @@ function isBootstrapLikeText(text) {
1530
1530
  return BOOTSTRAP_TEXT_MARKERS.some(marker => normalized.includes(marker));
1531
1531
  }
1532
1532
 
1533
- function removeLeadingSystemMessage(messages) {
1534
- if (!Array.isArray(messages) || messages.length === 0) {
1535
- return [];
1536
- }
1537
-
1538
- let startIndex = 1;
1539
- while (startIndex < messages.length) {
1540
- const item = messages[startIndex];
1541
- const role = item ? normalizeRole(item.role) : '';
1542
- const text = item && typeof item.text === 'string' ? item.text : '';
1543
- const isSystemRole = role === 'system';
1533
+ function removeLeadingSystemMessage(messages) {
1534
+ if (!Array.isArray(messages) || messages.length === 0) {
1535
+ return [];
1536
+ }
1537
+
1538
+ let startIndex = 0;
1539
+ while (startIndex < messages.length) {
1540
+ const item = messages[startIndex];
1541
+ const role = item ? normalizeRole(item.role) : '';
1542
+ const text = item && typeof item.text === 'string' ? item.text : '';
1543
+ const isSystemRole = role === 'system';
1544
1544
  const isBootstrapText = isBootstrapLikeText(text);
1545
1545
  if (!item || isSystemRole || isBootstrapText) {
1546
1546
  startIndex += 1;
@@ -1627,16 +1627,85 @@ function matchesSessionPathFilter(session, normalizedFilter) {
1627
1627
  return cwd.includes(normalizedFilter);
1628
1628
  }
1629
1629
 
1630
- function normalizeQueryTokens(query) {
1631
- if (typeof query !== 'string') {
1632
- return [];
1633
- }
1634
- return query
1635
- .split(/\s+/)
1636
- .map(item => item.trim())
1637
- .map(item => item.toLowerCase())
1638
- .filter(Boolean);
1639
- }
1630
+ function normalizeQueryTokens(query) {
1631
+ if (typeof query !== 'string') {
1632
+ return [];
1633
+ }
1634
+ return query
1635
+ .split(/\s+/)
1636
+ .map(item => item.trim())
1637
+ .map(item => item.toLowerCase())
1638
+ .filter(Boolean);
1639
+ }
1640
+
1641
+ function expandSessionQueryTokens(tokens) {
1642
+ const base = Array.isArray(tokens) ? tokens.map(t => String(t || '').toLowerCase()).filter(Boolean) : [];
1643
+ const result = [];
1644
+ const seen = new Set();
1645
+ let hasClaudeAlias = false;
1646
+ let hasDaudeAlias = false;
1647
+
1648
+ for (const token of base) {
1649
+ if (/^claude[-_ ]?code$/.test(token) || token === 'claudecode') {
1650
+ hasClaudeAlias = true;
1651
+ continue;
1652
+ }
1653
+ if (/^daude[-_ ]?code$/.test(token) || token === 'daudecode') {
1654
+ hasDaudeAlias = true;
1655
+ continue;
1656
+ }
1657
+ if (!seen.has(token)) {
1658
+ seen.add(token);
1659
+ result.push(token);
1660
+ }
1661
+ }
1662
+
1663
+ const push = (token) => {
1664
+ const normalized = String(token || '').toLowerCase();
1665
+ if (!normalized || seen.has(normalized)) return;
1666
+ seen.add(normalized);
1667
+ result.push(normalized);
1668
+ };
1669
+
1670
+ if (hasClaudeAlias) {
1671
+ push('claude');
1672
+ push('code');
1673
+ }
1674
+ if (hasDaudeAlias) {
1675
+ push('daude');
1676
+ push('code');
1677
+ }
1678
+
1679
+ return result;
1680
+ }
1681
+
1682
+ function normalizeKeywords(value) {
1683
+ if (!Array.isArray(value)) {
1684
+ return [];
1685
+ }
1686
+ const seen = new Set();
1687
+ const result = [];
1688
+ for (const item of value) {
1689
+ const normalized = typeof item === 'string' ? item.trim() : String(item || '').trim();
1690
+ if (!normalized) continue;
1691
+ const lower = normalized.toLowerCase();
1692
+ if (seen.has(lower)) continue;
1693
+ seen.add(lower);
1694
+ result.push(normalized);
1695
+ }
1696
+ return result;
1697
+ }
1698
+
1699
+ function normalizeCapabilities(value) {
1700
+ const result = {};
1701
+ if (!value || typeof value !== 'object') {
1702
+ return result;
1703
+ }
1704
+ if (value.code === true) {
1705
+ result.code = true;
1706
+ }
1707
+ return result;
1708
+ }
1640
1709
 
1641
1710
  function normalizeQueryMode(mode) {
1642
1711
  return mode === 'or' ? 'or' : 'and';
@@ -1671,18 +1740,22 @@ function matchTokensInText(text, tokens, mode = 'and') {
1671
1740
  return tokens.every(token => haystack.includes(token));
1672
1741
  }
1673
1742
 
1674
- function buildSessionSummaryText(session) {
1675
- if (!session) {
1676
- return '';
1677
- }
1678
- return [
1679
- session.title,
1680
- session.sessionId,
1681
- session.cwd,
1682
- session.filePath,
1683
- session.sourceLabel
1684
- ].filter(Boolean).join(' ');
1685
- }
1743
+ function buildSessionSummaryText(session) {
1744
+ if (!session) {
1745
+ return '';
1746
+ }
1747
+ const keywords = Array.isArray(session.keywords) ? session.keywords.join(' ') : '';
1748
+ const provider = typeof session.provider === 'string' ? session.provider : '';
1749
+ return [
1750
+ session.title,
1751
+ session.sessionId,
1752
+ session.cwd,
1753
+ session.filePath,
1754
+ session.sourceLabel,
1755
+ provider,
1756
+ keywords
1757
+ ].filter(Boolean).join(' ');
1758
+ }
1686
1759
 
1687
1760
  function extractMessageFromRecord(record, source) {
1688
1761
  if (!record) {
@@ -1792,39 +1865,39 @@ function applySessionQueryFilter(sessions, options = {}) {
1792
1865
  ? Math.max(1024, Number(options.contentScanBytes))
1793
1866
  : SESSION_CONTENT_READ_BYTES;
1794
1867
 
1795
- let scanned = 0;
1796
- const results = [];
1797
-
1798
- for (const session of sessions) {
1799
- if (scope === 'content' && scanned >= contentScanLimit) {
1868
+ let scanned = 0;
1869
+ const results = [];
1870
+
1871
+ for (const session of sessions) {
1872
+ if (scope === 'content' && scanned >= contentScanLimit) {
1800
1873
  break;
1801
1874
  }
1802
-
1803
- const summaryText = buildSessionSummaryText(session);
1804
- const summaryHit = scope !== 'content' && matchTokensInText(summaryText, tokens, mode);
1805
- let contentHit = false;
1806
- let contentInfo = null;
1807
-
1808
- if (scope !== 'summary' && (!summaryHit || scope === 'content')) {
1809
- if (scanned < contentScanLimit) {
1810
- scanned += 1;
1811
- contentInfo = scanSessionContentForQuery(session, tokens, {
1812
- mode,
1813
- roleFilter,
1814
- maxBytes: contentScanBytes,
1815
- maxMatches: 1,
1816
- snippetLimit: 2
1817
- });
1818
- contentHit = contentInfo.hit;
1819
- }
1820
- }
1821
-
1822
- const hit = scope === 'summary'
1823
- ? summaryHit
1824
- : (scope === 'content' ? contentHit : (summaryHit || contentHit));
1825
- if (!hit) {
1826
- continue;
1827
- }
1875
+
1876
+ const summaryText = buildSessionSummaryText(session);
1877
+ const summaryHit = scope !== 'content' && matchTokensInText(summaryText, tokens, mode);
1878
+ let contentHit = false;
1879
+ let contentInfo = null;
1880
+
1881
+ const shouldScanContent = scope === 'content' || scope === 'all' || !summaryHit;
1882
+ if (shouldScanContent && scanned < contentScanLimit) {
1883
+ scanned += 1;
1884
+ contentInfo = scanSessionContentForQuery(session, tokens, {
1885
+ mode,
1886
+ roleFilter,
1887
+ maxBytes: contentScanBytes,
1888
+ maxMatches: 1,
1889
+ snippetLimit: 2
1890
+ });
1891
+ contentHit = contentInfo.hit;
1892
+ }
1893
+
1894
+ const hit = scope === 'summary'
1895
+ ? summaryHit
1896
+ : (scope === 'content' ? contentHit : (summaryHit || contentHit));
1897
+
1898
+ if (!hit) {
1899
+ continue;
1900
+ }
1828
1901
 
1829
1902
  const matchInfo = contentInfo && contentInfo.hit
1830
1903
  ? contentInfo
@@ -1999,23 +2072,26 @@ function parseCodexSessionSummary(filePath) {
1999
2072
  }
2000
2073
  }
2001
2074
 
2002
- messageCount = Math.max(0, messageCount);
2003
-
2004
- return {
2005
- source: 'codex',
2006
- sourceLabel: 'Codex',
2007
- sessionId,
2008
- title: firstPrompt || sessionId,
2009
- cwd,
2010
- createdAt,
2011
- updatedAt,
2012
- messageCount,
2013
- filePath
2014
- };
2015
- }
2016
-
2017
- function parseClaudeSessionSummary(filePath) {
2018
- const records = parseJsonlHeadRecords(filePath);
2075
+ messageCount = Math.max(0, messageCount);
2076
+
2077
+ return {
2078
+ source: 'codex',
2079
+ sourceLabel: 'Codex',
2080
+ provider: 'codex',
2081
+ sessionId,
2082
+ title: firstPrompt || sessionId,
2083
+ cwd,
2084
+ createdAt,
2085
+ updatedAt,
2086
+ messageCount,
2087
+ filePath,
2088
+ keywords: [],
2089
+ capabilities: {}
2090
+ };
2091
+ }
2092
+
2093
+ function parseClaudeSessionSummary(filePath) {
2094
+ const records = parseJsonlHeadRecords(filePath);
2019
2095
  if (records.length === 0) {
2020
2096
  return null;
2021
2097
  }
@@ -2085,20 +2161,23 @@ function parseClaudeSessionSummary(filePath) {
2085
2161
  }
2086
2162
  }
2087
2163
 
2088
- messageCount = Math.max(0, messageCount);
2089
-
2090
- return {
2091
- source: 'claude',
2092
- sourceLabel: 'Claude Code',
2093
- sessionId,
2094
- title: firstPrompt || sessionId,
2095
- cwd,
2096
- createdAt,
2097
- updatedAt,
2098
- messageCount,
2099
- filePath
2100
- };
2101
- }
2164
+ messageCount = Math.max(0, messageCount);
2165
+
2166
+ return {
2167
+ source: 'claude',
2168
+ sourceLabel: 'Claude Code',
2169
+ provider: 'claude',
2170
+ sessionId,
2171
+ title: firstPrompt || sessionId,
2172
+ cwd,
2173
+ createdAt,
2174
+ updatedAt,
2175
+ messageCount,
2176
+ filePath,
2177
+ keywords: [],
2178
+ capabilities: { code: true }
2179
+ };
2180
+ }
2102
2181
 
2103
2182
  function listCodexSessions(limit, options = {}) {
2104
2183
  const codexSessionsDir = getCodexSessionsDir();
@@ -2199,12 +2278,12 @@ function listClaudeSessions(limit, options = {}) {
2199
2278
  let title = truncateText(entry.summary || entry.firstPrompt || sessionId, 120);
2200
2279
  let messageCount = Number.isFinite(entry.messageCount) ? Math.max(0, entry.messageCount - 1) : 0;
2201
2280
 
2202
- const quickRecords = parseJsonlHeadRecords(filePath, SESSION_TITLE_READ_BYTES);
2203
- if (quickRecords.length > 0) {
2204
- const filteredCount = countConversationMessagesInRecords(quickRecords, 'claude');
2205
- if (filteredCount > 0 || messageCount === 0) {
2206
- messageCount = filteredCount;
2207
- }
2281
+ const quickRecords = parseJsonlHeadRecords(filePath, SESSION_TITLE_READ_BYTES);
2282
+ if (quickRecords.length > 0) {
2283
+ const filteredCount = countConversationMessagesInRecords(quickRecords, 'claude');
2284
+ if (filteredCount > 0 || messageCount === 0) {
2285
+ messageCount = filteredCount;
2286
+ }
2208
2287
 
2209
2288
  const quickMessages = [];
2210
2289
  for (const record of quickRecords) {
@@ -2213,29 +2292,38 @@ function listClaudeSessions(limit, options = {}) {
2213
2292
  const content = record.message ? record.message.content : '';
2214
2293
  quickMessages.push({ role, text: extractMessageText(content) });
2215
2294
  }
2216
- }
2217
- const filteredQuickMessages = removeLeadingSystemMessage(quickMessages);
2218
- const firstUser = filteredQuickMessages.find(item => item.role === 'user' && item.text);
2219
- if (firstUser) {
2220
- title = truncateText(firstUser.text, 120);
2221
- }
2222
- }
2223
-
2224
- sessions.push({
2225
- source: 'claude',
2226
- sourceLabel: 'Claude Code',
2227
- sessionId,
2228
- title,
2229
- cwd: entry.projectPath || index.originalPath || '',
2230
- createdAt,
2231
- updatedAt,
2232
- messageCount,
2233
- filePath
2234
- });
2235
-
2236
- if (sessions.length >= targetCount) {
2237
- break;
2238
- }
2295
+ }
2296
+ const filteredQuickMessages = removeLeadingSystemMessage(quickMessages);
2297
+ const firstUser = filteredQuickMessages.find(item => item.role === 'user' && item.text);
2298
+ if (firstUser) {
2299
+ title = truncateText(firstUser.text, 120);
2300
+ }
2301
+ }
2302
+
2303
+ const provider = typeof entry.provider === 'string' && entry.provider.trim()
2304
+ ? entry.provider.trim()
2305
+ : 'claude';
2306
+ const keywords = normalizeKeywords(entry.keywords);
2307
+ const capabilities = normalizeCapabilities(entry.capabilities);
2308
+
2309
+ sessions.push({
2310
+ source: 'claude',
2311
+ sourceLabel: 'Claude Code',
2312
+ provider,
2313
+ sessionId,
2314
+ title,
2315
+ cwd: entry.projectPath || index.originalPath || '',
2316
+ createdAt,
2317
+ updatedAt,
2318
+ messageCount,
2319
+ filePath,
2320
+ keywords,
2321
+ capabilities
2322
+ });
2323
+
2324
+ if (sessions.length >= targetCount) {
2325
+ break;
2326
+ }
2239
2327
  }
2240
2328
 
2241
2329
  if (sessions.length >= targetCount) {
@@ -2268,15 +2356,15 @@ function listAllSessions(params = {}) {
2268
2356
  const source = params.source === 'codex' || params.source === 'claude'
2269
2357
  ? params.source
2270
2358
  : 'all';
2271
- const rawLimit = Number(params.limit);
2272
- const limit = Number.isFinite(rawLimit)
2273
- ? Math.max(1, Math.min(rawLimit, MAX_SESSION_LIST_SIZE))
2274
- : 120;
2275
- const forceRefresh = !!params.forceRefresh;
2276
- const normalizedPathFilter = normalizeSessionPathFilter(params.pathFilter);
2277
- const hasPathFilter = !!normalizedPathFilter;
2278
- const queryTokens = normalizeQueryTokens(params.query);
2279
- const hasQuery = queryTokens.length > 0;
2359
+ const rawLimit = Number(params.limit);
2360
+ const limit = Number.isFinite(rawLimit)
2361
+ ? Math.max(1, Math.min(rawLimit, MAX_SESSION_LIST_SIZE))
2362
+ : 120;
2363
+ const forceRefresh = !!params.forceRefresh;
2364
+ const normalizedPathFilter = normalizeSessionPathFilter(params.pathFilter);
2365
+ const hasPathFilter = !!normalizedPathFilter;
2366
+ const queryTokens = expandSessionQueryTokens(normalizeQueryTokens(params.query));
2367
+ const hasQuery = queryTokens.length > 0;
2280
2368
  const cacheKey = hasQuery ? '' : `${source}:${limit}:${normalizedPathFilter}`;
2281
2369
  if (!hasQuery) {
2282
2370
  const cached = getSessionListCache(cacheKey, forceRefresh);
@@ -2293,16 +2381,16 @@ function listAllSessions(params = {}) {
2293
2381
  : {};
2294
2382
 
2295
2383
  let sessions = [];
2296
- if (source === 'all' || source === 'codex') {
2297
- sessions = sessions.concat(listCodexSessions(limit, scanOptions));
2298
- }
2299
- if (source === 'all' || source === 'claude') {
2300
- sessions = sessions.concat(listClaudeSessions(limit, scanOptions));
2301
- }
2302
-
2303
- if (hasPathFilter) {
2304
- sessions = sessions.filter(item => matchesSessionPathFilter(item, normalizedPathFilter));
2305
- }
2384
+ if (source === 'all' || source === 'codex') {
2385
+ sessions = sessions.concat(listCodexSessions(limit, scanOptions));
2386
+ }
2387
+ if (source === 'all' || source === 'claude') {
2388
+ sessions = sessions.concat(listClaudeSessions(limit, scanOptions));
2389
+ }
2390
+
2391
+ if (hasPathFilter) {
2392
+ sessions = sessions.filter(item => matchesSessionPathFilter(item, normalizedPathFilter));
2393
+ }
2306
2394
 
2307
2395
  let result = sessions;
2308
2396
  if (hasQuery) {
@@ -2389,15 +2477,15 @@ function resolveSessionFilePath(source, filePath, sessionId) {
2389
2477
  }
2390
2478
  }
2391
2479
 
2392
- if (typeof sessionId === 'string' && sessionId.trim()) {
2393
- const targetId = sessionId.trim().toLowerCase();
2394
- const files = collectJsonlFiles(root, 5000);
2395
- const matchedFile = files.find(item => path.basename(item).toLowerCase().includes(targetId));
2396
- if (matchedFile && fs.existsSync(matchedFile)) {
2397
- return matchedFile;
2398
- }
2399
- }
2400
-
2480
+ if (typeof sessionId === 'string' && sessionId.trim()) {
2481
+ const targetId = sessionId.trim().toLowerCase();
2482
+ const files = collectJsonlFiles(root, 5000);
2483
+ const matchedFile = files.find(item => path.basename(item, '.jsonl').toLowerCase() === targetId);
2484
+ if (matchedFile && fs.existsSync(matchedFile)) {
2485
+ return matchedFile;
2486
+ }
2487
+ }
2488
+
2401
2489
  return '';
2402
2490
  }
2403
2491
 
@@ -4483,13 +4571,16 @@ function formatHostForUrl(host) {
4483
4571
  // 打开 Web UI
4484
4572
  function cmdStart(options = {}) {
4485
4573
  const htmlPath = path.join(__dirname, 'web-ui.html');
4574
+ const assetsDir = path.join(__dirname, 'res');
4575
+ const webDir = path.join(__dirname, 'web-ui');
4486
4576
  if (!fs.existsSync(htmlPath)) {
4487
4577
  console.error('错误: web-ui.html 不存在');
4488
4578
  process.exit(1);
4489
4579
  }
4490
4580
 
4491
4581
  const server = http.createServer((req, res) => {
4492
- if (req.url === '/api') {
4582
+ const requestPath = (req.url || '/').split('?')[0];
4583
+ if (requestPath === '/api') {
4493
4584
  let body = '';
4494
4585
  req.on('data', chunk => body += chunk);
4495
4586
  req.on('end', async () => {
@@ -4670,6 +4761,50 @@ function cmdStart(options = {}) {
4670
4761
  res.end(JSON.stringify({ error: e.message }));
4671
4762
  }
4672
4763
  });
4764
+ } else if (requestPath.startsWith('/web-ui/')) {
4765
+ const normalized = path.normalize(requestPath).replace(/^([\\.\\/])+/, '');
4766
+ const filePath = path.join(__dirname, normalized);
4767
+ if (!isPathInside(filePath, webDir)) {
4768
+ res.writeHead(403, { 'Content-Type': 'text/plain; charset=utf-8' });
4769
+ res.end('Forbidden');
4770
+ return;
4771
+ }
4772
+ if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) {
4773
+ res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
4774
+ res.end('Not Found');
4775
+ return;
4776
+ }
4777
+ const ext = path.extname(filePath).toLowerCase();
4778
+ const mime = ext === '.js' || ext === '.mjs'
4779
+ ? 'application/javascript; charset=utf-8'
4780
+ : ext === '.css'
4781
+ ? 'text/css; charset=utf-8'
4782
+ : ext === '.json'
4783
+ ? 'application/json; charset=utf-8'
4784
+ : 'application/octet-stream';
4785
+ res.writeHead(200, { 'Content-Type': mime });
4786
+ fs.createReadStream(filePath).pipe(res);
4787
+ } else if (requestPath.startsWith('/res/')) {
4788
+ const normalized = path.normalize(requestPath).replace(/^([\\.\\/])+/, '');
4789
+ const filePath = path.join(__dirname, normalized);
4790
+ if (!isPathInside(filePath, assetsDir)) {
4791
+ res.writeHead(403, { 'Content-Type': 'text/plain; charset=utf-8' });
4792
+ res.end('Forbidden');
4793
+ return;
4794
+ }
4795
+ if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) {
4796
+ res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
4797
+ res.end('Not Found');
4798
+ return;
4799
+ }
4800
+ const ext = path.extname(filePath).toLowerCase();
4801
+ const mime = ext === '.js'
4802
+ ? 'application/javascript; charset=utf-8'
4803
+ : ext === '.json'
4804
+ ? 'application/json; charset=utf-8'
4805
+ : 'application/octet-stream';
4806
+ res.writeHead(200, { 'Content-Type': mime });
4807
+ fs.createReadStream(filePath).pipe(res);
4673
4808
  } else {
4674
4809
  const html = fs.readFileSync(htmlPath, 'utf-8');
4675
4810
  res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
@@ -111,15 +111,13 @@ function writeJsonAtomic(filePath, data) {
111
111
 
112
112
  const tmpPath = `${filePath}.tmp-${process.pid}-${Date.now()}`;
113
113
  const content = `${JSON.stringify(data, null, 2)}\n`;
114
+ const hasExisting = fs.existsSync(filePath);
115
+ const targetMode = hasExisting ? (fs.statSync(filePath).mode & 0o777) : 0o600;
114
116
 
115
117
  try {
116
- fs.writeFileSync(tmpPath, content, 'utf-8');
117
- if (fs.existsSync(filePath)) {
118
- const existingMode = fs.statSync(filePath).mode;
119
- fs.chmodSync(tmpPath, existingMode);
120
- } else {
121
- fs.chmodSync(tmpPath, 0o600);
122
- }
118
+ fs.writeFileSync(tmpPath, content, { encoding: 'utf-8', mode: targetMode });
119
+ fs.chmodSync(tmpPath, targetMode);
120
+
123
121
  try {
124
122
  fs.renameSync(tmpPath, filePath);
125
123
  } catch (renameError) {
@@ -130,6 +128,10 @@ function writeJsonAtomic(filePath, data) {
130
128
  throw renameError;
131
129
  }
132
130
  }
131
+
132
+ try {
133
+ fs.chmodSync(filePath, targetMode);
134
+ } catch (_) {}
133
135
  } catch (e) {
134
136
  if (fs.existsSync(tmpPath)) {
135
137
  try { fs.unlinkSync(tmpPath); } catch (_) {}
package/package.json CHANGED
@@ -1,13 +1,14 @@
1
1
  {
2
2
  "name": "codexmate",
3
- "version": "0.0.8",
3
+ "version": "0.0.10",
4
4
  "description": "Codex/Claude Code 配置与会话管理 CLI + Web 工具",
5
5
  "bin": {
6
6
  "codexmate": "./cli.js"
7
7
  },
8
8
  "scripts": {
9
9
  "start": "node cli.js",
10
- "test": "npm run test:e2e",
10
+ "test": "npm run test:unit && npm run test:e2e",
11
+ "test:unit": "node tests/unit/run.mjs",
11
12
  "test:e2e": "node tests/e2e/run.js"
12
13
  },
13
14
  "dependencies": {