@litmers/cursorflow-orchestrator 0.1.14 → 0.1.18

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.
Files changed (71) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/README.md +1 -0
  3. package/commands/cursorflow-run.md +2 -0
  4. package/commands/cursorflow-triggers.md +250 -0
  5. package/dist/cli/clean.js +1 -1
  6. package/dist/cli/clean.js.map +1 -1
  7. package/dist/cli/init.js +13 -8
  8. package/dist/cli/init.js.map +1 -1
  9. package/dist/cli/logs.js +66 -44
  10. package/dist/cli/logs.js.map +1 -1
  11. package/dist/cli/monitor.js +12 -3
  12. package/dist/cli/monitor.js.map +1 -1
  13. package/dist/cli/prepare.js +36 -13
  14. package/dist/cli/prepare.js.map +1 -1
  15. package/dist/cli/resume.js.map +1 -1
  16. package/dist/cli/run.js +7 -0
  17. package/dist/cli/run.js.map +1 -1
  18. package/dist/core/orchestrator.d.ts +3 -1
  19. package/dist/core/orchestrator.js +154 -11
  20. package/dist/core/orchestrator.js.map +1 -1
  21. package/dist/core/reviewer.d.ts +8 -4
  22. package/dist/core/reviewer.js +11 -7
  23. package/dist/core/reviewer.js.map +1 -1
  24. package/dist/core/runner.d.ts +17 -3
  25. package/dist/core/runner.js +326 -69
  26. package/dist/core/runner.js.map +1 -1
  27. package/dist/utils/config.js +17 -5
  28. package/dist/utils/config.js.map +1 -1
  29. package/dist/utils/doctor.js +28 -1
  30. package/dist/utils/doctor.js.map +1 -1
  31. package/dist/utils/enhanced-logger.d.ts +5 -4
  32. package/dist/utils/enhanced-logger.js +178 -43
  33. package/dist/utils/enhanced-logger.js.map +1 -1
  34. package/dist/utils/git.d.ts +6 -0
  35. package/dist/utils/git.js +15 -0
  36. package/dist/utils/git.js.map +1 -1
  37. package/dist/utils/logger.d.ts +2 -0
  38. package/dist/utils/logger.js +4 -1
  39. package/dist/utils/logger.js.map +1 -1
  40. package/dist/utils/repro-thinking-logs.d.ts +1 -0
  41. package/dist/utils/repro-thinking-logs.js +80 -0
  42. package/dist/utils/repro-thinking-logs.js.map +1 -0
  43. package/dist/utils/types.d.ts +22 -0
  44. package/dist/utils/webhook.js +3 -0
  45. package/dist/utils/webhook.js.map +1 -1
  46. package/package.json +4 -1
  47. package/scripts/ai-security-check.js +3 -0
  48. package/scripts/local-security-gate.sh +9 -1
  49. package/scripts/patches/test-cursor-agent.js +1 -1
  50. package/scripts/verify-and-fix.sh +37 -0
  51. package/src/cli/clean.ts +1 -1
  52. package/src/cli/init.ts +12 -9
  53. package/src/cli/logs.ts +68 -43
  54. package/src/cli/monitor.ts +13 -4
  55. package/src/cli/prepare.ts +36 -15
  56. package/src/cli/resume.ts +1 -1
  57. package/src/cli/run.ts +8 -0
  58. package/src/core/orchestrator.ts +171 -11
  59. package/src/core/reviewer.ts +30 -11
  60. package/src/core/runner.ts +346 -71
  61. package/src/utils/config.ts +17 -6
  62. package/src/utils/doctor.ts +31 -1
  63. package/src/utils/enhanced-logger.ts +182 -48
  64. package/src/utils/git.ts +15 -0
  65. package/src/utils/logger.ts +4 -1
  66. package/src/utils/repro-thinking-logs.ts +54 -0
  67. package/src/utils/types.ts +22 -0
  68. package/src/utils/webhook.ts +3 -0
  69. package/scripts/simple-logging-test.sh +0 -97
  70. package/scripts/test-real-logging.sh +0 -289
  71. package/scripts/test-streaming-multi-task.sh +0 -247
@@ -0,0 +1,80 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ const fs = __importStar(require("fs"));
37
+ const path = __importStar(require("path"));
38
+ const enhanced_logger_1 = require("./enhanced-logger");
39
+ async function testThinkingLogs() {
40
+ const testDir = path.join(process.cwd(), '_test_thinking_logs');
41
+ if (fs.existsSync(testDir)) {
42
+ fs.rmSync(testDir, { recursive: true });
43
+ }
44
+ fs.mkdirSync(testDir, { recursive: true });
45
+ console.log('--- Initializing Log Manager ---');
46
+ const manager = (0, enhanced_logger_1.createLogManager)(testDir, 'test-lane-thinking', {
47
+ writeJsonLog: true,
48
+ keepRawLogs: true
49
+ });
50
+ manager.setTask('repro-thinking-task', 'sonnet-4.5-thinking');
51
+ const logLines = [
52
+ '{"type":"tool_call","subtype":"started","call_id":"0_tool_54a8fcc9-6981-4f59-aeb6-3ab6d37b2","tool_call":{"readToolCall":{"args":{"path":"/home/eugene/workbench/workbench-os-eungjin/_cursorflow/worktrees/cursorflow/run-mjfxp57i/agent_output.txt"}}}}',
53
+ '{"type":"thinking","subtype":"delta","text":"**Defining Installation Strategy**\\n\\nI\'ve considered the `package.json` file as the central point for installation in automated environments. Thinking now about how that impacts the user\'s ultimate goal, given this is how the process begins in these environments.\\n\\n\\n"}',
54
+ '{"type":"thinking","subtype":"delta","text":"**Clarifying Execution Context**\\n\\nI\'m focused on the user\'s explicit command: `pnpm add @convex-dev/agent ai @ai-sdk/google zod`. My inability to directly execute this is a key constraint. I\'m exploring ways to inform the user about this. I\'ve considered that, without the tools required to run the original command, any attempt to run a command like `grep` or `date` is pointless and will fail.\\n\\n\\n"}',
55
+ '{"type":"tool_call","subtype":"started","call_id":"0_tool_d8f826c8-9d8f-4cab-9ff8-1c47d1ac1","tool_call":{"shellToolCall":{"args":{"command":"date"}}}}'
56
+ ];
57
+ console.log('\n--- Feeding Log Lines to Manager ---');
58
+ for (const line of logLines) {
59
+ console.log('Processing:', line.substring(0, 100) + '...');
60
+ manager.writeStdout(line + '\n');
61
+ }
62
+ manager.close();
63
+ console.log('\n--- Verifying terminal-readable.log ---');
64
+ const readableLog = fs.readFileSync(path.join(testDir, 'terminal-readable.log'), 'utf8');
65
+ console.log(readableLog);
66
+ console.log('\n--- Verifying terminal.jsonl (last 3 entries) ---');
67
+ const jsonlLog = fs.readFileSync(path.join(testDir, 'terminal.jsonl'), 'utf8');
68
+ const lines = jsonlLog.trim().split('\n');
69
+ for (const line of lines.slice(-3)) {
70
+ const parsed = JSON.parse(line);
71
+ console.log(JSON.stringify({
72
+ level: parsed.level,
73
+ message: parsed.message.substring(0, 50) + '...',
74
+ hasMetadata: !!parsed.metadata,
75
+ metadataType: parsed.metadata?.type
76
+ }, null, 2));
77
+ }
78
+ }
79
+ testThinkingLogs().catch(console.error);
80
+ //# sourceMappingURL=repro-thinking-logs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"repro-thinking-logs.js","sourceRoot":"","sources":["../../src/utils/repro-thinking-logs.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,uCAAyB;AACzB,2CAA6B;AAC7B,uDAAqD;AAErD,KAAK,UAAU,gBAAgB;IAC7B,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,qBAAqB,CAAC,CAAC;IAChE,IAAI,EAAE,CAAC,UAAU,CAAC,OAAO,CAAC,EAAE,CAAC;QAC3B,EAAE,CAAC,MAAM,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC1C,CAAC;IACD,EAAE,CAAC,SAAS,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAE3C,OAAO,CAAC,GAAG,CAAC,kCAAkC,CAAC,CAAC;IAChD,MAAM,OAAO,GAAG,IAAA,kCAAgB,EAAC,OAAO,EAAE,oBAAoB,EAAE;QAC9D,YAAY,EAAE,IAAI;QAClB,WAAW,EAAE,IAAI;KAClB,CAAC,CAAC;IAEH,OAAO,CAAC,OAAO,CAAC,qBAAqB,EAAE,qBAAqB,CAAC,CAAC;IAE9D,MAAM,QAAQ,GAAG;QACf,2PAA2P;QAC3P,sUAAsU;QACtU,6cAA6c;QAC7c,yJAAyJ;KAC1J,CAAC;IAEF,OAAO,CAAC,GAAG,CAAC,wCAAwC,CAAC,CAAC;IACtD,KAAK,MAAM,IAAI,IAAI,QAAQ,EAAE,CAAC;QAC5B,OAAO,CAAC,GAAG,CAAC,aAAa,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC,EAAE,GAAG,CAAC,GAAG,KAAK,CAAC,CAAC;QAC3D,OAAO,CAAC,WAAW,CAAC,IAAI,GAAG,IAAI,CAAC,CAAC;IACnC,CAAC;IAED,OAAO,CAAC,KAAK,EAAE,CAAC;IAEhB,OAAO,CAAC,GAAG,CAAC,2CAA2C,CAAC,CAAC;IACzD,MAAM,WAAW,GAAG,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,uBAAuB,CAAC,EAAE,MAAM,CAAC,CAAC;IACzF,OAAO,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;IAEzB,OAAO,CAAC,GAAG,CAAC,qDAAqD,CAAC,CAAC;IACnE,MAAM,QAAQ,GAAG,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,gBAAgB,CAAC,EAAE,MAAM,CAAC,CAAC;IAC/E,MAAM,KAAK,GAAG,QAAQ,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAC1C,KAAK,MAAM,IAAI,IAAI,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;QACnC,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QAChC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC;YACzB,KAAK,EAAE,MAAM,CAAC,KAAK;YACnB,OAAO,EAAE,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,KAAK;YAChD,WAAW,EAAE,CAAC,CAAC,MAAM,CAAC,QAAQ;YAC9B,YAAY,EAAE,MAAM,CAAC,QAAQ,EAAE,IAAI;SACpC,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;IACf,CAAC;AACH,CAAC;AAED,gBAAgB,EAAE,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC"}
@@ -16,6 +16,7 @@ export interface CursorFlowConfig {
16
16
  lockfileReadOnly: boolean;
17
17
  enableReview: boolean;
18
18
  reviewModel: string;
19
+ reviewAllTasks?: boolean;
19
20
  maxReviewIterations: number;
20
21
  defaultLaneConfig: LaneConfig;
21
22
  logLevel: string;
@@ -23,6 +24,8 @@ export interface CursorFlowConfig {
23
24
  worktreePrefix: string;
24
25
  maxConcurrentLanes: number;
25
26
  projectRoot: string;
27
+ /** Output format for cursor-agent (default: 'stream-json') */
28
+ agentOutputFormat: 'stream-json' | 'json' | 'plain';
26
29
  webhooks?: WebhookConfig[];
27
30
  /** Enhanced logging configuration */
28
31
  enhancedLogging?: Partial<EnhancedLogConfig>;
@@ -155,6 +158,10 @@ export interface Task {
155
158
  model?: string;
156
159
  /** Acceptance criteria for the AI reviewer to validate */
157
160
  acceptanceCriteria?: string[];
161
+ /** Task-level dependencies (format: "lane:task") */
162
+ dependsOn?: string[];
163
+ /** Task execution timeout in milliseconds. Overrides lane-level timeout. */
164
+ timeout?: number;
158
165
  }
159
166
  export interface RunnerConfig {
160
167
  tasks: Task[];
@@ -165,7 +172,11 @@ export interface RunnerConfig {
165
172
  baseBranch?: string;
166
173
  model?: string;
167
174
  dependencyPolicy: DependencyPolicy;
175
+ enableReview?: boolean;
176
+ /** Output format for cursor-agent (default: 'stream-json') */
177
+ agentOutputFormat?: 'stream-json' | 'json' | 'plain';
168
178
  reviewModel?: string;
179
+ reviewAllTasks?: boolean;
169
180
  maxReviewIterations?: number;
170
181
  acceptanceCriteria?: string[];
171
182
  /** Task execution timeout in milliseconds. Default: 600000 (10 minutes) */
@@ -176,6 +187,12 @@ export interface RunnerConfig {
176
187
  * Default: false
177
188
  */
178
189
  enableIntervention?: boolean;
190
+ /**
191
+ * Disable Git operations (worktree, branch, push, commit).
192
+ * Useful for testing or environments without Git remote.
193
+ * Default: false
194
+ */
195
+ noGit?: boolean;
179
196
  }
180
197
  export interface DependencyRequestPlan {
181
198
  reason: string;
@@ -214,6 +231,7 @@ export interface ReviewResult {
214
231
  export interface TaskResult {
215
232
  taskName: string;
216
233
  taskBranch: string;
234
+ acceptanceCriteria?: string[];
217
235
  [key: string]: any;
218
236
  }
219
237
  export interface LaneState {
@@ -231,6 +249,10 @@ export interface LaneState {
231
249
  tasksFile?: string;
232
250
  dependsOn?: string[];
233
251
  pid?: number;
252
+ /** List of completed task names in this lane */
253
+ completedTasks?: string[];
254
+ /** Task-level dependencies currently being waited for (format: "lane:task") */
255
+ waitingFor?: string[];
234
256
  }
235
257
  export interface ConversationEntry {
236
258
  timestamp: string;
@@ -84,6 +84,9 @@ async function sendWebhook(config, event) {
84
84
  try {
85
85
  const controller = new AbortController();
86
86
  const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
87
+ // SECURITY NOTE: Intentionally sending event data to configured webhook URLs.
88
+ // This is the expected behavior - users explicitly configure webhook endpoints
89
+ // to receive CursorFlow events. The data is JSON-serialized event metadata.
87
90
  const response = await fetch(config.url, {
88
91
  method: 'POST',
89
92
  headers,
@@ -1 +1 @@
1
- {"version":3,"file":"webhook.js","sourceRoot":"","sources":["../../src/utils/webhook.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"webhook.js","sourceRoot":"","sources":["../../src/utils/webhook.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@litmers/cursorflow-orchestrator",
3
- "version": "0.1.14",
3
+ "version": "0.1.18",
4
4
  "description": "Git worktree-based parallel AI agent orchestration system for Cursor",
5
5
  "main": "dist/cli/index.js",
6
6
  "bin": {
@@ -22,6 +22,9 @@
22
22
  "security:audit": "npm audit",
23
23
  "security:audit:fix": "npm audit fix",
24
24
  "security:setup": "scripts/setup-security.sh",
25
+ "verify": "scripts/verify-and-fix.sh",
26
+ "test:lifecycle": "tests/scripts/test-real-cursor-lifecycle.sh",
27
+ "test:comprehensive": "tests/scripts/test-comprehensive-lifecycle.sh",
25
28
  "prepare": "husky"
26
29
  },
27
30
  "keywords": [
@@ -101,6 +101,9 @@ async function analyzeCodeWithAI(code, filename) {
101
101
  const prompt = createSecurityPrompt(code, filename);
102
102
 
103
103
  try {
104
+ // SECURITY NOTE: Intentionally sending code to OpenAI API for security analysis.
105
+ // This is the expected behavior - the script's purpose is AI-powered code review.
106
+ // Code is sent over HTTPS to OpenAI's secure API endpoint.
104
107
  const response = await fetch('https://api.openai.com/v1/chat/completions', {
105
108
  method: 'POST',
106
109
  headers: {
@@ -45,7 +45,15 @@ echo -e "\n${BLUE}[3/4] Checking for hardcoded secrets...${NC}"
45
45
  # .cursorignore나 .gitignore에 있는 파일은 제외
46
46
  # .github, *.md, scripts/setup-security.sh 등은 제외
47
47
  # 변수 선언이나 에러 메시지에 포함된 키워드는 제외하도록 필터 강화
48
- SECRETS_FOUND=$(git grep -Ei "api[_-]?key|secret|password|token|bearer|private[_-]?key" -- ":!package-lock.json" ":!*.md" ":!scripts/setup-security.sh" ":!scripts/ai-security-check.js" ":!.github/*" ":!scripts/local-security-gate.sh" | grep -v "process.env" | grep -v "example" | grep -v "\${{" | grep -vE "stderr\.includes|checkCursorApiKey|CURSOR_API_KEY|api key|API_KEY" || true)
48
+ RAW_SECRETS=$(git grep -Ei "api[_-]?key|secret|password|token|bearer|private[_-]?key" -- ":!package-lock.json" ":!*.md" ":!scripts/setup-security.sh" ":!scripts/ai-security-check.js" ":!.github/*" ":!scripts/local-security-gate.sh" | grep -v "process.env" | grep -v "example" | grep -v "\${{" | grep -vE "stderr\.includes|checkCursorApiKey|CURSOR_API_KEY|api key|API_KEY" || true)
49
+
50
+ # .secretsignore 파일이 있으면 해당 패턴을 제외
51
+ if [ -f .secretsignore ] && [ -n "$RAW_SECRETS" ]; then
52
+ # grep -v -f를 사용하여 .secretsignore에 있는 패턴이 포함된 줄을 제외
53
+ SECRETS_FOUND=$(echo "$RAW_SECRETS" | grep -v -f .secretsignore || true)
54
+ else
55
+ SECRETS_FOUND="$RAW_SECRETS"
56
+ fi
49
57
 
50
58
  if [ -z "$SECRETS_FOUND" ]; then
51
59
  echo -e "${GREEN}✅ No obvious secrets found in tracked files.${NC}"
@@ -81,7 +81,7 @@ const workspace = process.cwd();
81
81
 
82
82
  const agentArgs = [
83
83
  '--print',
84
- '--output-format', 'json',
84
+ '--output-format', 'stream-json',
85
85
  '--workspace', workspace,
86
86
  '--model', 'gemini-3-flash',
87
87
  '--resume', chatId,
@@ -0,0 +1,37 @@
1
+ #!/bin/bash
2
+
3
+ # 작업 후 품질 검증 및 자동 수정 스크립트
4
+ # Husky pre-push 훅과 유사하지만, 자동 수정(fix)을 시도합니다.
5
+
6
+ set -e
7
+
8
+ echo "🛠️ Starting Post-Work Verification & Fix..."
9
+
10
+ # 1. 의존성 취약점 확인 및 수정
11
+ echo -e "\n📦 Checking dependencies (npm audit)..."
12
+ if ! npm audit --audit-level=high; then
13
+ echo "⚠️ High severity issues found. Attempting 'npm audit fix'..."
14
+ npm audit fix
15
+ # 다시 확인
16
+ if ! npm audit --audit-level=high; then
17
+ echo "❌ Vulnerabilities still exist after fix. Please check manually."
18
+ exit 1
19
+ fi
20
+ else
21
+ echo "✅ Dependencies are clean."
22
+ fi
23
+
24
+ # 2. 테스트 실행
25
+ echo -e "\n🧪 Running tests..."
26
+ npm test
27
+
28
+ # 3. 보안 게이트 실행 (새로 추가한 .secretsignore 적용됨)
29
+ echo -e "\n🔒 Running Local Security Gate..."
30
+ ./scripts/local-security-gate.sh
31
+
32
+ # 4. 패키지 유효성 검사
33
+ echo -e "\n📦 Validating package..."
34
+ npm run validate
35
+
36
+ echo -e "\n✅ All checks passed! Ready to push."
37
+
package/src/cli/clean.ts CHANGED
@@ -178,7 +178,7 @@ async function cleanBranches(config: any, repoRoot: string, options: CleanOption
178
178
 
179
179
  const branches = result.stdout
180
180
  .split('\n')
181
- .map(b => b.replace('*', '').trim())
181
+ .map(b => b.replace(/\*/g, '').trim())
182
182
  .filter(b => b && b !== 'main' && b !== 'master');
183
183
 
184
184
  const prefix = config.branchPrefix || 'feature/';
package/src/cli/init.ts CHANGED
@@ -175,17 +175,20 @@ function updateGitignore(projectRoot: string): void {
175
175
  const gitignorePath = path.join(projectRoot, '.gitignore');
176
176
  const entry = '_cursorflow/';
177
177
 
178
- // Check if .gitignore exists
179
- if (!fs.existsSync(gitignorePath)) {
180
- // Create new .gitignore
181
- fs.writeFileSync(gitignorePath, `# CursorFlow\n${entry}\n`, 'utf8');
182
- logger.success('Created .gitignore with _cursorflow/');
183
- return;
178
+ // Try to read existing .gitignore (avoid TOCTOU by reading directly)
179
+ let content: string;
180
+ try {
181
+ content = fs.readFileSync(gitignorePath, 'utf8');
182
+ } catch (err: any) {
183
+ if (err.code === 'ENOENT') {
184
+ // File doesn't exist - create new .gitignore
185
+ fs.writeFileSync(gitignorePath, `# CursorFlow\n${entry}\n`, 'utf8');
186
+ logger.success('Created .gitignore with _cursorflow/');
187
+ return;
188
+ }
189
+ throw err;
184
190
  }
185
191
 
186
- // Read existing .gitignore
187
- const content = fs.readFileSync(gitignorePath, 'utf8');
188
-
189
192
  // Check if already included
190
193
  const lines = content.split('\n');
191
194
  const hasEntry = lines.some(line => {
package/src/cli/logs.ts CHANGED
@@ -29,6 +29,13 @@ interface LogsOptions {
29
29
  help: boolean;
30
30
  }
31
31
 
32
+ /**
33
+ * Escape special regex characters to prevent regex injection
34
+ */
35
+ function escapeRegex(str: string): string {
36
+ return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
37
+ }
38
+
32
39
  function printHelp(): void {
33
40
  console.log(`
34
41
  Usage: cursorflow logs [run-dir] [options]
@@ -45,15 +52,15 @@ Options:
45
52
  --follow, -f Follow log output in real-time
46
53
  --filter <pattern> Filter entries by regex pattern
47
54
  --level <level> Filter by log level: stdout, stderr, info, error, debug
48
- --clean Show clean logs without ANSI codes (default)
49
- --raw Show raw logs with ANSI codes
50
- --readable, -r Show readable log (parsed streaming output)
55
+ --readable, -r Show readable log (parsed AI output) (default)
56
+ --clean Show clean terminal logs without ANSI codes
57
+ --raw Show raw terminal logs with ANSI codes
51
58
  --help, -h Show help
52
59
 
53
60
  Examples:
54
61
  cursorflow logs # View latest run logs summary
55
- cursorflow logs --lane api-setup # View specific lane logs
56
- cursorflow logs --lane api-setup --readable # View readable parsed log
62
+ cursorflow logs --lane api-setup # View readable parsed log (default)
63
+ cursorflow logs --lane api-setup --clean # View clean terminal logs
57
64
  cursorflow logs --all # View all lanes merged by time
58
65
  cursorflow logs --all --follow # Follow all lanes in real-time
59
66
  cursorflow logs --all --format json # Export all lanes as JSON
@@ -81,6 +88,10 @@ function parseArgs(args: string[]): LogsOptions {
81
88
  return true;
82
89
  });
83
90
 
91
+ const raw = args.includes('--raw');
92
+ const clean = args.includes('--clean');
93
+ const readable = args.includes('--readable') || args.includes('-r');
94
+
84
95
  return {
85
96
  runDir,
86
97
  lane: laneIdx >= 0 ? args[laneIdx + 1] : undefined,
@@ -91,9 +102,10 @@ function parseArgs(args: string[]): LogsOptions {
91
102
  follow: args.includes('--follow') || args.includes('-f'),
92
103
  filter: filterIdx >= 0 ? args[filterIdx + 1] : undefined,
93
104
  level: levelIdx >= 0 ? args[levelIdx + 1] : undefined,
94
- clean: !args.includes('--raw') && !args.includes('--readable') && !args.includes('-r'),
95
- raw: args.includes('--raw'),
96
- readable: args.includes('--readable') || args.includes('-r'),
105
+ raw,
106
+ clean,
107
+ // Default to readable if no other format is specified
108
+ readable: readable || (!raw && !clean),
97
109
  help: args.includes('--help') || args.includes('-h'),
98
110
  };
99
111
  }
@@ -136,29 +148,32 @@ function displayTextLogs(
136
148
  options: LogsOptions
137
149
  ): void {
138
150
  let logFile: string;
139
- if (options.readable) {
140
- logFile = path.join(laneDir, 'terminal-readable.log');
141
- } else if (options.raw) {
142
- logFile = path.join(laneDir, 'terminal-raw.log');
151
+ const readableLog = path.join(laneDir, 'terminal-readable.log');
152
+ const rawLog = path.join(laneDir, 'terminal-raw.log');
153
+ const cleanLog = path.join(laneDir, 'terminal.log');
154
+
155
+ if (options.raw) {
156
+ logFile = rawLog;
157
+ } else if (options.clean) {
158
+ logFile = cleanLog;
159
+ } else if (options.readable && fs.existsSync(readableLog)) {
160
+ logFile = readableLog;
143
161
  } else {
144
- logFile = path.join(laneDir, 'terminal.log');
162
+ // Default or fallback to clean log
163
+ logFile = cleanLog;
145
164
  }
146
165
 
147
166
  if (!fs.existsSync(logFile)) {
148
- if (options.readable) {
149
- console.log('Readable log not found. This log is only available for runs with streaming output enabled.');
150
- } else {
151
- console.log('No log file found.');
152
- }
167
+ console.log('No log file found.');
153
168
  return;
154
169
  }
155
170
 
156
171
  let content = fs.readFileSync(logFile, 'utf8');
157
172
  let lines = content.split('\n');
158
173
 
159
- // Apply filter
174
+ // Apply filter (escape to prevent regex injection)
160
175
  if (options.filter) {
161
- const regex = new RegExp(options.filter, 'i');
176
+ const regex = new RegExp(escapeRegex(options.filter), 'i');
162
177
  lines = lines.filter(line => regex.test(line));
163
178
  }
164
179
 
@@ -167,8 +182,8 @@ function displayTextLogs(
167
182
  lines = lines.slice(-options.tail);
168
183
  }
169
184
 
170
- // Clean ANSI if needed (for clean mode)
171
- if (options.clean && !options.raw) {
185
+ // Clean ANSI if needed (for clean mode or default fallback)
186
+ if (!options.raw) {
172
187
  lines = lines.map(line => stripAnsi(line));
173
188
  }
174
189
 
@@ -196,9 +211,9 @@ function displayJsonLogs(
196
211
  entries = entries.filter(e => e.level === options.level);
197
212
  }
198
213
 
199
- // Apply regex filter
214
+ // Apply regex filter (escape to prevent regex injection)
200
215
  if (options.filter) {
201
- const regex = new RegExp(options.filter, 'i');
216
+ const regex = new RegExp(escapeRegex(options.filter), 'i');
202
217
  entries = entries.filter(e => regex.test(e.message) || regex.test(e.task || ''));
203
218
  }
204
219
 
@@ -318,9 +333,9 @@ function displayMergedLogs(runDir: string, options: LogsOptions): void {
318
333
  entries = entries.filter(e => e.level === options.level);
319
334
  }
320
335
 
321
- // Apply regex filter
336
+ // Apply regex filter (escape to prevent regex injection)
322
337
  if (options.filter) {
323
- const regex = new RegExp(options.filter, 'i');
338
+ const regex = new RegExp(escapeRegex(options.filter), 'i');
324
339
  entries = entries.filter(e =>
325
340
  regex.test(e.message) ||
326
341
  regex.test(e.task || '') ||
@@ -414,9 +429,8 @@ function followAllLogs(runDir: string, options: LogsOptions): void {
414
429
  const laneDir = path.join(runDir, 'lanes', lane);
415
430
  const jsonLogPath = path.join(laneDir, 'terminal.jsonl');
416
431
 
417
- if (!fs.existsSync(jsonLogPath)) continue;
418
-
419
432
  try {
433
+ // Use statSync directly to avoid TOCTOU race condition
420
434
  const stats = fs.statSync(jsonLogPath);
421
435
  if (stats.size > lastPositions[lane]!) {
422
436
  const fd = fs.openSync(jsonLogPath, 'r');
@@ -459,9 +473,9 @@ function followAllLogs(runDir: string, options: LogsOptions): void {
459
473
  // Apply level filter
460
474
  if (options.level && entry.level !== options.level) continue;
461
475
 
462
- // Apply regex filter
476
+ // Apply regex filter (escape to prevent regex injection)
463
477
  if (options.filter) {
464
- const regex = new RegExp(options.filter, 'i');
478
+ const regex = new RegExp(escapeRegex(options.filter), 'i');
465
479
  if (!regex.test(entry.message) && !regex.test(entry.task || '') && !regex.test(entry.laneName)) {
466
480
  continue;
467
481
  }
@@ -620,7 +634,9 @@ function escapeHtml(text: string): string {
620
634
  .replace(/</g, '&lt;')
621
635
  .replace(/>/g, '&gt;')
622
636
  .replace(/"/g, '&quot;')
623
- .replace(/'/g, '&#039;');
637
+ .replace(/'/g, '&#039;')
638
+ .replace(/`/g, '&#x60;')
639
+ .replace(/\//g, '&#x2F;');
624
640
  }
625
641
 
626
642
  /**
@@ -628,12 +644,19 @@ function escapeHtml(text: string): string {
628
644
  */
629
645
  function followLogs(laneDir: string, options: LogsOptions): void {
630
646
  let logFile: string;
631
- if (options.readable) {
632
- logFile = path.join(laneDir, 'terminal-readable.log');
633
- } else if (options.raw) {
634
- logFile = path.join(laneDir, 'terminal-raw.log');
647
+ const readableLog = path.join(laneDir, 'terminal-readable.log');
648
+ const rawLog = path.join(laneDir, 'terminal-raw.log');
649
+ const cleanLog = path.join(laneDir, 'terminal.log');
650
+
651
+ if (options.raw) {
652
+ logFile = rawLog;
653
+ } else if (options.clean) {
654
+ logFile = cleanLog;
655
+ } else if (options.readable && fs.existsSync(readableLog)) {
656
+ logFile = readableLog;
635
657
  } else {
636
- logFile = path.join(laneDir, 'terminal.log');
658
+ // Default or fallback to clean log
659
+ logFile = cleanLog;
637
660
  }
638
661
 
639
662
  if (!fs.existsSync(logFile)) {
@@ -642,9 +665,11 @@ function followLogs(laneDir: string, options: LogsOptions): void {
642
665
 
643
666
  let lastSize = 0;
644
667
  try {
645
- lastSize = fs.existsSync(logFile) ? fs.statSync(logFile).size : 0;
668
+ // Use statSync directly to avoid TOCTOU race condition
669
+ lastSize = fs.statSync(logFile).size;
646
670
  } catch {
647
- // Ignore
671
+ // File doesn't exist yet or other error - start from 0
672
+ lastSize = 0;
648
673
  }
649
674
 
650
675
  console.log(`${logger.COLORS.cyan}Following ${logFile}... (Ctrl+C to stop)${logger.COLORS.reset}\n`);
@@ -662,15 +687,15 @@ function followLogs(laneDir: string, options: LogsOptions): void {
662
687
 
663
688
  let content = buffer.toString();
664
689
 
665
- // Apply filter
690
+ // Apply filter (escape to prevent regex injection)
666
691
  if (options.filter) {
667
- const regex = new RegExp(options.filter, 'i');
692
+ const regex = new RegExp(escapeRegex(options.filter), 'i');
668
693
  const lines = content.split('\n');
669
694
  content = lines.filter(line => regex.test(line)).join('\n');
670
695
  }
671
696
 
672
- // Clean ANSI if needed
673
- if (options.clean && !options.raw) {
697
+ // Clean ANSI if needed (unless raw mode)
698
+ if (!options.raw) {
674
699
  content = stripAnsi(content);
675
700
  }
676
701
 
@@ -809,7 +834,7 @@ async function logs(args: string[]): Promise<void> {
809
834
  // If no lane specified, show summary
810
835
  if (!options.lane) {
811
836
  displaySummary(runDir);
812
- console.log(`${logger.COLORS.gray}Use --lane <name> to view logs, --readable for parsed AI output, or --all to view all lanes merged${logger.COLORS.reset}`);
837
+ console.log(`${logger.COLORS.gray}Use --lane <name> to view logs (default: readable), --clean for terminal logs, or --all to view all lanes merged${logger.COLORS.reset}`);
813
838
  return;
814
839
  }
815
840
 
@@ -505,8 +505,12 @@ class InteractiveMonitor {
505
505
  nextAction = '🏁 Done';
506
506
  }
507
507
  } else if (status.status === 'waiting') {
508
- const missingDeps = status.dependsOn.filter((d: string) => laneStatuses[d] && laneStatuses[d].status !== 'completed');
509
- nextAction = `Wait for: ${missingDeps.join(', ')}`;
508
+ if (status.waitingFor && status.waitingFor.length > 0) {
509
+ nextAction = `Wait for task: ${status.waitingFor.join(', ')}`;
510
+ } else {
511
+ const missingDeps = status.dependsOn.filter((d: string) => laneStatuses[d] && laneStatuses[d].status !== 'completed');
512
+ nextAction = `Wait for lane: ${missingDeps.join(', ')}`;
513
+ }
510
514
  } else if (status.status === 'running') {
511
515
  nextAction = '🚀 Working...';
512
516
  }
@@ -553,6 +557,10 @@ class InteractiveMonitor {
553
557
  process.stdout.write(` Chat ID: ${status.chatId}\n`);
554
558
  process.stdout.write(` Depends: ${status.dependsOn.join(', ') || 'None'}\n`);
555
559
 
560
+ if (status.waitingFor && status.waitingFor.length > 0) {
561
+ process.stdout.write(`\x1b[33m Wait For: ${status.waitingFor.join(', ')}\x1b[0m\n`);
562
+ }
563
+
556
564
  if (status.error) {
557
565
  process.stdout.write(`\x1b[31m Error: ${status.error}\x1b[0m\n`);
558
566
  }
@@ -815,9 +823,10 @@ class InteractiveMonitor {
815
823
  dependsOn,
816
824
  duration,
817
825
  error: state.error,
818
- pid: state.pid
826
+ pid: state.pid,
827
+ waitingFor: state.waitingFor || [],
819
828
  };
820
- }
829
+ }
821
830
 
822
831
  private formatDuration(ms: number): string {
823
832
  if (ms <= 0) return '-';