@jojonax/codex-copilot 1.0.2 → 1.1.0

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.
@@ -1,59 +1,76 @@
1
1
  /**
2
- * codex-copilot status - 显示当前进度
2
+ * codex-copilot status - Show current progress
3
3
  */
4
4
 
5
- import { readFileSync } from 'fs';
5
+ import { readFileSync, existsSync } from 'fs';
6
6
  import { resolve } from 'path';
7
7
  import { log, progressBar } from '../utils/logger.js';
8
+ import { createCheckpoint } from '../utils/checkpoint.js';
8
9
 
9
10
  export async function status(projectDir) {
10
11
  const tasksPath = resolve(projectDir, '.codex-copilot/tasks.json');
11
- const statePath = resolve(projectDir, '.codex-copilot/state.json');
12
+ const checkpoint = createCheckpoint(projectDir);
12
13
 
13
14
  let tasks, state;
14
15
  try {
15
16
  tasks = JSON.parse(readFileSync(tasksPath, 'utf-8'));
16
- state = JSON.parse(readFileSync(statePath, 'utf-8'));
17
+ state = checkpoint.load();
17
18
  } catch (err) {
18
- log.error(`读取文件失败: ${err.message}`);
19
- log.warn('文件可能已损坏,请运行 codex-copilot reset');
19
+ log.error(`Failed to read files: ${err.message}`);
20
+ log.warn('Files may be corrupted. Run: codex-copilot reset');
20
21
  return;
21
22
  }
22
23
 
23
- log.title(`📊 项目: ${tasks.project}`);
24
+ log.title(`📊 Project: ${tasks.project}`);
24
25
  log.blank();
25
26
 
26
- // 进度条
27
+ // Progress bar
27
28
  const completed = tasks.tasks.filter(t => t.status === 'completed').length;
28
29
  const inProgress = tasks.tasks.filter(t => t.status === 'in_progress' || t.status === 'developed').length;
29
30
  const pending = tasks.tasks.filter(t => t.status === 'pending').length;
30
31
  const skipped = tasks.tasks.filter(t => t.status === 'skipped').length;
31
32
 
32
- progressBar(completed, tasks.total, `${completed}/${tasks.total} 完成`);
33
+ progressBar(completed, tasks.total, `${completed}/${tasks.total} done`);
33
34
  log.blank();
34
35
 
35
- // 统计
36
- log.info(`✅ 已完成: ${completed}`);
37
- if (inProgress > 0) log.info(`🔄 进行中: ${inProgress}`);
38
- log.info(`⏳ 待开发: ${pending}`);
39
- if (skipped > 0) log.warn(`⏭ 已跳过: ${skipped}`);
36
+ // Stats
37
+ log.info(`✅ Completed: ${completed}`);
38
+ if (inProgress > 0) log.info(`🔄 In progress: ${inProgress}`);
39
+ log.info(`⏳ Pending: ${pending}`);
40
+ if (skipped > 0) log.warn(`⏭ Skipped: ${skipped}`);
40
41
  log.blank();
41
42
 
42
- // 当前状态
43
- if (state.current_pr) {
44
- log.info(`当前 PR: #${state.current_pr} (Review 第 ${state.review_round} 轮)`);
43
+ // Current checkpoint state
44
+ if (state.phase) {
45
+ const phaseLabels = {
46
+ develop: '🔨 Develop',
47
+ pr: '📦 PR',
48
+ review: '👀 Review',
49
+ merge: '🔀 Merge',
50
+ };
51
+ const phaseLabel = phaseLabels[state.phase] || state.phase;
52
+ log.info(`📍 Checkpoint: Task #${state.current_task} — ${phaseLabel} → ${state.phase_step}`);
53
+ if (state.branch) log.dim(` Branch: ${state.branch}`);
54
+ if (state.current_pr) log.dim(` PR: #${state.current_pr}`);
55
+ if (state.review_round > 0) log.dim(` Review round: ${state.review_round}`);
56
+ if (state.last_updated) log.dim(` Last saved: ${state.last_updated}`);
57
+ } else if (state.current_task > 0) {
58
+ log.info(`Last completed task: #${state.current_task}`);
45
59
  }
46
60
  log.blank();
47
61
 
48
- // 任务列表
49
- log.title('任务列表:');
62
+ // Task list
63
+ log.title('Task list:');
50
64
  log.blank();
51
65
  for (const task of tasks.tasks) {
66
+ const isCurrentTask = state.current_task === task.id && state.phase;
52
67
  const icon = task.status === 'completed' ? '✅' :
68
+ isCurrentTask ? '🔄' :
53
69
  task.status === 'in_progress' ? '🔄' :
54
70
  task.status === 'developed' ? '📦' :
55
71
  task.status === 'skipped' ? '⏭ ' : '⬜';
56
- console.log(` ${icon} #${task.id} ${task.title} [${task.branch}]`);
72
+ const suffix = isCurrentTask ? ` ${state.phase}:${state.phase_step}` : '';
73
+ console.log(` ${icon} #${task.id} ${task.title} [${task.branch}]${suffix}`);
57
74
  }
58
75
  log.blank();
59
76
  }
@@ -0,0 +1,279 @@
1
+ /**
2
+ * Desktop IDE Automator — platform-aware process detection + auto-paste
3
+ *
4
+ * Supports macOS (AppleScript/osascript) and Windows (PowerShell/SendKeys).
5
+ * Each IDE has its own keystroke recipe.
6
+ * Falls back gracefully if automation fails.
7
+ */
8
+
9
+ import { execSync } from 'child_process';
10
+ import { log } from './logger.js';
11
+
12
+ const IS_MAC = process.platform === 'darwin';
13
+ const IS_WIN = process.platform === 'win32';
14
+
15
+ // ──────────────────────────────────────────────
16
+ // Per-IDE Automation Recipes
17
+ // ──────────────────────────────────────────────
18
+
19
+ /**
20
+ * Each recipe defines:
21
+ * processName: { mac, win } — binary/process name to detect
22
+ * appName: { mac, win } — display name for activation
23
+ * preKeys: keystroke sequence BEFORE paste (e.g., open Composer in Cursor)
24
+ * preDelay: ms to wait after preKeys
25
+ * postKeys: keystroke sequence AFTER paste (usually Enter to send)
26
+ * pasteDelay: ms to wait between paste and postKeys
27
+ */
28
+ const IDE_RECIPES = {
29
+ 'codex-desktop': {
30
+ processName: { mac: 'Codex', win: 'Codex' },
31
+ appName: { mac: 'Codex', win: 'Codex' },
32
+ // Codex Desktop: input is focused on activate → paste → Enter
33
+ preKeys: null,
34
+ preDelay: 0,
35
+ postKeys: 'return',
36
+ pasteDelay: 300,
37
+ },
38
+
39
+ 'cursor': {
40
+ processName: { mac: 'Cursor', win: 'Cursor' },
41
+ appName: { mac: 'Cursor', win: 'Cursor' },
42
+ // Cursor: Cmd/Ctrl+I opens Composer, then paste, then Enter
43
+ preKeys: IS_MAC
44
+ ? 'keystroke "i" using {command down}'
45
+ : '^i',
46
+ preDelay: 800,
47
+ postKeys: 'return',
48
+ pasteDelay: 300,
49
+ },
50
+
51
+ 'antigravity': {
52
+ processName: { mac: 'Antigravity', win: 'Antigravity' },
53
+ appName: { mac: 'Antigravity', win: 'Antigravity' },
54
+ // Antigravity: chat input auto-focused → paste → Enter
55
+ preKeys: null,
56
+ preDelay: 0,
57
+ postKeys: 'return',
58
+ pasteDelay: 300,
59
+ },
60
+ };
61
+
62
+ // ──────────────────────────────────────────────
63
+ // Process Detection
64
+ // ──────────────────────────────────────────────
65
+
66
+ /**
67
+ * Check if a process is running
68
+ * @param {string} processName - Name of the process
69
+ * @returns {boolean}
70
+ */
71
+ export function isProcessRunning(processName) {
72
+ try {
73
+ if (IS_MAC) {
74
+ const result = execSync(
75
+ `pgrep -x "${processName}" || pgrep -fi "${processName}"`,
76
+ { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }
77
+ ).trim();
78
+ return result.length > 0;
79
+ } else if (IS_WIN) {
80
+ const result = execSync(
81
+ `tasklist /FI "IMAGENAME eq ${processName}.exe" /NH`,
82
+ { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }
83
+ ).trim();
84
+ return !result.includes('No tasks');
85
+ }
86
+ return false;
87
+ } catch {
88
+ return false;
89
+ }
90
+ }
91
+
92
+ // ──────────────────────────────────────────────
93
+ // macOS Automation (AppleScript)
94
+ // ──────────────────────────────────────────────
95
+
96
+ function buildAppleScript(recipe) {
97
+ const appName = recipe.appName.mac;
98
+ const lines = [];
99
+
100
+ // Activate the application
101
+ lines.push(`tell application "${appName}" to activate`);
102
+ lines.push('delay 0.5');
103
+
104
+ // System Events block for keystrokes
105
+ lines.push('tell application "System Events"');
106
+
107
+ // Pre-keys (e.g., Cmd+I for Cursor Composer)
108
+ if (recipe.preKeys) {
109
+ lines.push(` ${recipe.preKeys}`);
110
+ lines.push(` delay ${recipe.preDelay / 1000}`);
111
+ }
112
+
113
+ // Paste: Cmd+V
114
+ lines.push(' keystroke "v" using {command down}');
115
+ lines.push(` delay ${recipe.pasteDelay / 1000}`);
116
+
117
+ // Post-keys: usually Enter to send
118
+ if (recipe.postKeys === 'return') {
119
+ lines.push(' keystroke return');
120
+ }
121
+
122
+ lines.push('end tell');
123
+
124
+ return lines.join('\n');
125
+ }
126
+
127
+ function runAppleScript(script) {
128
+ try {
129
+ execSync(`osascript -e '${script.replace(/'/g, "'\"'\"'")}'`, {
130
+ encoding: 'utf-8',
131
+ stdio: ['pipe', 'pipe', 'pipe'],
132
+ timeout: 10000,
133
+ });
134
+ return true;
135
+ } catch (err) {
136
+ log.warn(`AppleScript failed: ${err.message}`);
137
+ if (err.message.includes('not allowed assistive access')) {
138
+ log.error('⚠ Accessibility permission required:');
139
+ log.error(' System Settings → Privacy & Security → Accessibility');
140
+ log.error(' Add your terminal app (Terminal / iTerm2 / Warp)');
141
+ }
142
+ return false;
143
+ }
144
+ }
145
+
146
+ // ──────────────────────────────────────────────
147
+ // Windows Automation (PowerShell)
148
+ // ──────────────────────────────────────────────
149
+
150
+ function buildPowerShell(recipe) {
151
+ const appName = recipe.appName.win;
152
+ const lines = [];
153
+
154
+ lines.push('Add-Type -AssemblyName System.Windows.Forms');
155
+ lines.push('Add-Type -AssemblyName Microsoft.VisualBasic');
156
+
157
+ // Activate window
158
+ lines.push(`[Microsoft.VisualBasic.Interaction]::AppActivate("${appName}")`);
159
+ lines.push('Start-Sleep -Milliseconds 500');
160
+
161
+ // Pre-keys
162
+ if (recipe.preKeys) {
163
+ lines.push(`[System.Windows.Forms.SendKeys]::SendWait("${recipe.preKeys}")`);
164
+ lines.push(`Start-Sleep -Milliseconds ${recipe.preDelay}`);
165
+ }
166
+
167
+ // Paste: Ctrl+V
168
+ lines.push('[System.Windows.Forms.SendKeys]::SendWait("^v")');
169
+ lines.push(`Start-Sleep -Milliseconds ${recipe.pasteDelay}`);
170
+
171
+ // Post-keys: Enter
172
+ if (recipe.postKeys === 'return') {
173
+ lines.push('[System.Windows.Forms.SendKeys]::SendWait("{ENTER}")');
174
+ }
175
+
176
+ return lines.join('; ');
177
+ }
178
+
179
+ function runPowerShell(script) {
180
+ try {
181
+ execSync(`powershell -NoProfile -Command "${script.replace(/"/g, '\\"')}"`, {
182
+ encoding: 'utf-8',
183
+ stdio: ['pipe', 'pipe', 'pipe'],
184
+ timeout: 10000,
185
+ });
186
+ return true;
187
+ } catch (err) {
188
+ log.warn(`PowerShell automation failed: ${err.message}`);
189
+ return false;
190
+ }
191
+ }
192
+
193
+ // ──────────────────────────────────────────────
194
+ // Public API
195
+ // ──────────────────────────────────────────────
196
+
197
+ /**
198
+ * Check if a specific IDE has a known automation recipe
199
+ */
200
+ export function hasRecipe(providerId) {
201
+ return providerId in IDE_RECIPES;
202
+ }
203
+
204
+ /**
205
+ * Check if the IDE is currently running
206
+ */
207
+ export function isIDERunning(providerId) {
208
+ const recipe = IDE_RECIPES[providerId];
209
+ if (!recipe) return false;
210
+
211
+ const processName = IS_MAC ? recipe.processName.mac : recipe.processName.win;
212
+ return isProcessRunning(processName);
213
+ }
214
+
215
+ /**
216
+ * Attempt to auto-paste prompt into the IDE and trigger send
217
+ *
218
+ * Flow: copy to clipboard → activate window → pre-keys → paste → post-keys
219
+ *
220
+ * @param {string} providerId - IDE provider ID
221
+ * @param {string} text - Prompt text to paste
222
+ * @returns {boolean} true if automation succeeded
223
+ */
224
+ export function activateAndPaste(providerId, text) {
225
+ const recipe = IDE_RECIPES[providerId];
226
+ if (!recipe) {
227
+ log.warn(`No automation recipe for '${providerId}'`);
228
+ return false;
229
+ }
230
+
231
+ // Step 1: Check process is running
232
+ const processName = IS_MAC ? recipe.processName.mac : recipe.processName.win;
233
+ if (!isProcessRunning(processName)) {
234
+ log.warn(`${recipe.appName[IS_MAC ? 'mac' : 'win']} is not running`);
235
+ return false;
236
+ }
237
+
238
+ // Step 2: Copy to clipboard (handled by caller, but ensure it's done)
239
+ copyToSystemClipboard(text);
240
+
241
+ // Step 3: Run platform-specific automation
242
+ log.info(`Auto-pasting into ${recipe.appName[IS_MAC ? 'mac' : 'win']}...`);
243
+
244
+ if (IS_MAC) {
245
+ const script = buildAppleScript(recipe);
246
+ return runAppleScript(script);
247
+ } else if (IS_WIN) {
248
+ const script = buildPowerShell(recipe);
249
+ return runPowerShell(script);
250
+ } else {
251
+ log.warn('Desktop automation not supported on this platform');
252
+ return false;
253
+ }
254
+ }
255
+
256
+ /**
257
+ * Copy text to system clipboard (cross-platform)
258
+ */
259
+ function copyToSystemClipboard(text) {
260
+ try {
261
+ if (IS_MAC) {
262
+ execSync('pbcopy', { input: text, stdio: ['pipe', 'pipe', 'pipe'] });
263
+ } else if (IS_WIN) {
264
+ execSync('clip', { input: text, stdio: ['pipe', 'pipe', 'pipe'] });
265
+ } else {
266
+ try {
267
+ execSync('xclip -selection clipboard', { input: text, stdio: ['pipe', 'pipe', 'pipe'] });
268
+ } catch {
269
+ execSync('xsel --clipboard --input', { input: text, stdio: ['pipe', 'pipe', 'pipe'] });
270
+ }
271
+ }
272
+ } catch {
273
+ // Clipboard failure is non-fatal — user can copy file manually
274
+ }
275
+ }
276
+
277
+ export const automator = {
278
+ isProcessRunning, hasRecipe, isIDERunning, activateAndPaste,
279
+ };
@@ -0,0 +1,129 @@
1
+ /**
2
+ * Checkpoint state manager - Fine-grained state persistence
3
+ *
4
+ * Provides atomic save/load for checkpoint state, enabling
5
+ * resume from any sub-step within a task after interruption.
6
+ */
7
+
8
+ import { readFileSync, writeFileSync, renameSync, existsSync } from 'fs';
9
+ import { resolve } from 'path';
10
+
11
+ /**
12
+ * Create a checkpoint manager for a project
13
+ * @param {string} projectDir - Project root directory
14
+ */
15
+ export function createCheckpoint(projectDir) {
16
+ const statePath = resolve(projectDir, '.codex-copilot/state.json');
17
+ const tempPath = resolve(projectDir, '.codex-copilot/state.json.tmp');
18
+
19
+ /**
20
+ * Load current state from disk
21
+ */
22
+ function load() {
23
+ try {
24
+ return JSON.parse(readFileSync(statePath, 'utf-8'));
25
+ } catch {
26
+ return getDefaultState();
27
+ }
28
+ }
29
+
30
+ /**
31
+ * Save state atomically (write to temp file, then rename)
32
+ * @param {object} update - Partial state to merge
33
+ */
34
+ function save(update) {
35
+ const current = load();
36
+ const merged = {
37
+ ...current,
38
+ ...update,
39
+ last_updated: new Date().toISOString(),
40
+ };
41
+ // Atomic write: temp file + rename prevents corruption on crash
42
+ writeFileSync(tempPath, JSON.stringify(merged, null, 2));
43
+ renameSync(tempPath, statePath);
44
+ return merged;
45
+ }
46
+
47
+ /**
48
+ * Mark a specific phase & step as reached
49
+ */
50
+ function saveStep(taskId, phase, phaseStep, extra = {}) {
51
+ return save({
52
+ current_task: taskId,
53
+ phase,
54
+ phase_step: phaseStep,
55
+ ...extra,
56
+ });
57
+ }
58
+
59
+ /**
60
+ * Mark current task as fully completed, clear phase info
61
+ */
62
+ function completeTask(taskId) {
63
+ return save({
64
+ current_task: taskId,
65
+ phase: null,
66
+ phase_step: null,
67
+ current_pr: null,
68
+ review_round: 0,
69
+ branch: null,
70
+ });
71
+ }
72
+
73
+ /**
74
+ * Check if a specific phase+step has been reached for a task
75
+ * Returns true if the saved state is at or past the given step
76
+ */
77
+ function isStepDone(taskId, phase, phaseStep) {
78
+ const state = load();
79
+ if (state.current_task !== taskId) return false;
80
+ if (!state.phase) return false;
81
+
82
+ const phaseOrder = ['develop', 'pr', 'review', 'merge'];
83
+ const savedPhaseIdx = phaseOrder.indexOf(state.phase);
84
+ const targetPhaseIdx = phaseOrder.indexOf(phase);
85
+
86
+ // If saved phase is ahead, this step is done
87
+ if (savedPhaseIdx > targetPhaseIdx) return true;
88
+ // If saved phase is behind, this step is not done
89
+ if (savedPhaseIdx < targetPhaseIdx) return false;
90
+
91
+ // Same phase: compare steps within phase
92
+ const stepOrders = {
93
+ develop: ['branch_created', 'prompt_ready', 'codex_complete'],
94
+ pr: ['committed', 'pushed', 'pr_created'],
95
+ review: ['waiting_review', 'feedback_received', 'fix_applied'],
96
+ merge: ['merged'],
97
+ };
98
+
99
+ const steps = stepOrders[phase] || [];
100
+ const savedStepIdx = steps.indexOf(state.phase_step);
101
+ const targetStepIdx = steps.indexOf(phaseStep);
102
+
103
+ return savedStepIdx >= targetStepIdx;
104
+ }
105
+
106
+ /**
107
+ * Full reset to initial state
108
+ */
109
+ function reset() {
110
+ const state = getDefaultState();
111
+ writeFileSync(statePath, JSON.stringify(state, null, 2));
112
+ return state;
113
+ }
114
+
115
+ return { load, save, saveStep, completeTask, isStepDone, reset };
116
+ }
117
+
118
+ function getDefaultState() {
119
+ return {
120
+ current_task: 0,
121
+ phase: null,
122
+ phase_step: null,
123
+ current_pr: null,
124
+ review_round: 0,
125
+ branch: null,
126
+ status: 'initialized',
127
+ last_updated: new Date().toISOString(),
128
+ };
129
+ }
@@ -1,13 +1,13 @@
1
1
  /**
2
- * PRD 自动检测模块
3
- * 在项目目录中自动查找 PRD 文档
2
+ * PRD auto-detection module
3
+ * Automatically finds PRD documents in the project directory
4
4
  */
5
5
 
6
6
  import { readdirSync, readFileSync, statSync } from 'fs';
7
7
  import { resolve, extname, relative } from 'path';
8
8
  import { log } from './logger.js';
9
9
 
10
- // PRD 文件名匹配模式(优先级由高到低)
10
+ // PRD filename patterns (ordered by priority, highest first)
11
11
  const PRD_PATTERNS = [
12
12
  /prd/i,
13
13
  /product.?requirement/i,
@@ -18,9 +18,9 @@ const PRD_PATTERNS = [
18
18
  /spec/i,
19
19
  ];
20
20
 
21
- // 搜索目录(按优先级)
21
+ // Directories to search (ordered by priority)
22
22
  const SEARCH_DIRS = [
23
- '.', // 项目根目录
23
+ '.', // Project root
24
24
  'docs',
25
25
  'doc',
26
26
  'PRD',
@@ -30,16 +30,16 @@ const SEARCH_DIRS = [
30
30
  '文档',
31
31
  ];
32
32
 
33
- // 忽略目录
33
+ // Directories to ignore
34
34
  const IGNORE_DIRS = new Set([
35
35
  'node_modules', '.git', '.next', 'dist', 'build', 'vendor',
36
36
  '.codex-copilot', '.vscode', '.idea', '__pycache__', 'coverage',
37
37
  ]);
38
38
 
39
39
  /**
40
- * 自动检测当前项目中的 PRD 文件
41
- * @param {string} projectDir - 项目根目录
42
- * @returns {Array<{path: string, score: number, name: string}>} 候选 PRD 文件列表(按匹配度排序)
40
+ * Auto-detect PRD files in the project
41
+ * @param {string} projectDir - Project root directory
42
+ * @returns {Array<{path: string, score: number, name: string}>} Candidate PRD files sorted by match score
43
43
  */
44
44
  export function detectPRD(projectDir) {
45
45
  const candidates = [];
@@ -55,14 +55,14 @@ export function detectPRD(projectDir) {
55
55
  scanDir(searchPath, projectDir, candidates, 0);
56
56
  }
57
57
 
58
- // 按匹配度排序
58
+ // Sort by match score (descending)
59
59
  candidates.sort((a, b) => b.score - a.score);
60
60
 
61
61
  return candidates;
62
62
  }
63
63
 
64
64
  function scanDir(dir, projectDir, candidates, depth) {
65
- if (depth > 3) return; // 最多搜索 3
65
+ if (depth > 3) return; // Max 3 levels deep
66
66
 
67
67
  let entries;
68
68
  try {
@@ -81,10 +81,10 @@ function scanDir(dir, projectDir, candidates, depth) {
81
81
  continue;
82
82
  }
83
83
 
84
- // 只搜索 markdown 文件
84
+ // Only scan markdown files
85
85
  if (extname(entry.name).toLowerCase() !== '.md') continue;
86
86
 
87
- // 计算匹配分数
87
+ // Calculate match score
88
88
  const score = scorePRDMatch(entry.name, fullPath);
89
89
  if (score > 0) {
90
90
  candidates.push({
@@ -101,21 +101,21 @@ function scorePRDMatch(filename, fullPath) {
101
101
  let score = 0;
102
102
  const lower = filename.toLowerCase();
103
103
 
104
- // 文件名匹配
104
+ // Filename pattern matching
105
105
  for (let i = 0; i < PRD_PATTERNS.length; i++) {
106
106
  if (PRD_PATTERNS[i].test(filename)) {
107
- score += (PRD_PATTERNS.length - i) * 10; // 越靠前的模式分数越高
107
+ score += (PRD_PATTERNS.length - i) * 10; // Higher priority patterns get higher scores
108
108
  }
109
109
  }
110
110
 
111
- // 文件大小加分(PRD 通常内容较多)
111
+ // File size bonus (PRDs are typically longer)
112
112
  try {
113
113
  const stat = statSync(fullPath);
114
114
  if (stat.size > 5000) score += 5; // > 5KB
115
115
  if (stat.size > 20000) score += 5; // > 20KB
116
116
  } catch {}
117
117
 
118
- // 内容采样检测(读前 2000 字符)
118
+ // Content sampling (read first 2000 chars)
119
119
  if (score > 0) {
120
120
  try {
121
121
  const content = readFileSync(fullPath, 'utf-8').slice(0, 2000);
@@ -130,7 +130,7 @@ function scorePRDMatch(filename, fullPath) {
130
130
  }
131
131
 
132
132
  /**
133
- * 读取 PRD 文件内容
133
+ * Read PRD file content
134
134
  */
135
135
  export function readPRD(prdPath) {
136
136
  return readFileSync(prdPath, 'utf-8');