threadlines 0.2.25 → 0.3.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.
- package/README.md +87 -42
- package/dist/api/client.js +4 -0
- package/dist/commands/check.js +139 -93
- package/dist/commands/init.js +32 -23
- package/dist/llm/prompt-builder.js +72 -0
- package/dist/processors/expert.js +120 -0
- package/dist/processors/single-expert.js +197 -0
- package/dist/utils/config-file.js +13 -4
- package/dist/utils/config.js +20 -14
- package/dist/utils/diff-filter.js +105 -0
- package/dist/utils/logger.js +13 -6
- package/dist/utils/slim-diff.js +133 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -4,17 +4,17 @@ Threadline CLI - AI-powered linter based on your natural language documentation.
|
|
|
4
4
|
|
|
5
5
|
## Why Threadline?
|
|
6
6
|
|
|
7
|
-
Getting teams to follow
|
|
7
|
+
Getting teams to consistently follow coding patterns and quality standards is **hard**. Really hard.
|
|
8
8
|
|
|
9
9
|
- **Documentation** → Nobody reads it. Or it's outdated before you finish writing it.
|
|
10
10
|
- **Linting** → Catches syntax errors, but misses nuanced stuff.
|
|
11
11
|
- **AI Code Reviewers** → Powerful, but you can't trust them. Did they actually check what you care about? Can you customize them with your team's specific rules?
|
|
12
12
|
|
|
13
|
-
**Threadline solves this** by running **separate, parallel, highly focused AI-powered reviews** - each focused on a single, specific concern. Your coding
|
|
13
|
+
**Threadline solves this** by running **separate, parallel, highly focused AI-powered reviews** - each focused on a single, specific concern or pattern: the stuff that takes engineers months to internalise - and they keep forgetting. Your coding patterns live in your repository as 'Threadline' markdown files, version-controlled and always in sync with your codebase. Each threadline is its own AI agent, ensuring focused attention on what matters to your team.
|
|
14
14
|
|
|
15
15
|
### What Makes Threadline Different?
|
|
16
16
|
|
|
17
|
-
- **Focused Reviews** - Instead of one AI
|
|
17
|
+
- **Focused Reviews** - Instead of one AI agent checking everything, Threadline runs multiple specialized AI reviewers in parallel. Each threadline focuses on one thing and does it well.
|
|
18
18
|
|
|
19
19
|
- **Documentation That Lives With Your Code** - Your coding standards live in your repo, in a `/threadlines` folder. They're version-controlled, reviewable, and always in sync with your codebase.
|
|
20
20
|
|
|
@@ -24,18 +24,7 @@ Getting teams to follow consistent quality standards is **hard**. Really hard.
|
|
|
24
24
|
|
|
25
25
|
## Installation
|
|
26
26
|
|
|
27
|
-
### Option 1:
|
|
28
|
-
|
|
29
|
-
```bash
|
|
30
|
-
npm install -g threadlines
|
|
31
|
-
```
|
|
32
|
-
|
|
33
|
-
Then use directly:
|
|
34
|
-
```bash
|
|
35
|
-
threadlines check
|
|
36
|
-
```
|
|
37
|
-
|
|
38
|
-
### Option 2: Use with npx (No Installation)
|
|
27
|
+
### Option 1: Use with npx (No Installation)
|
|
39
28
|
|
|
40
29
|
```bash
|
|
41
30
|
npx threadlines check
|
|
@@ -82,8 +71,17 @@ Edit `threadlines/example.md` with your coding standards, then rename it to some
|
|
|
82
71
|
```bash
|
|
83
72
|
npx threadlines check
|
|
84
73
|
```
|
|
74
|
+
|
|
85
75
|
By default, analyzes your staged/unstaged git changes against all threadlines in the `/threadlines` directory.
|
|
86
76
|
|
|
77
|
+
**Review Context Types:**
|
|
78
|
+
- `local` - Staged/unstaged changes (default for local development)
|
|
79
|
+
- `commit` - Specific commit (when using `--commit` flag)
|
|
80
|
+
- `pr` - Pull Request/Merge Request (auto-detected in CI)
|
|
81
|
+
- `file` - Single file (when using `--file` flag)
|
|
82
|
+
- `folder` - Folder contents (when using `--folder` flag)
|
|
83
|
+
- `files` - Multiple files (when using `--files` flag)
|
|
84
|
+
|
|
87
85
|
**Common Use Cases:**
|
|
88
86
|
|
|
89
87
|
**Check latest commit locally:**
|
|
@@ -96,11 +94,6 @@ threadlines check --commit HEAD
|
|
|
96
94
|
threadlines check --commit abc123def
|
|
97
95
|
```
|
|
98
96
|
|
|
99
|
-
**Check all commits in a branch:**
|
|
100
|
-
```bash
|
|
101
|
-
threadlines check --branch feature/new-feature
|
|
102
|
-
```
|
|
103
|
-
|
|
104
97
|
**Check entire file(s):**
|
|
105
98
|
```bash
|
|
106
99
|
threadlines check --file src/api/users.ts
|
|
@@ -108,24 +101,35 @@ threadlines check --files src/api/users.ts src/api/posts.ts
|
|
|
108
101
|
threadlines check --folder src/api
|
|
109
102
|
```
|
|
110
103
|
|
|
104
|
+
**Debug mode (verbose output):**
|
|
105
|
+
```bash
|
|
106
|
+
threadlines check --debug
|
|
107
|
+
```
|
|
108
|
+
|
|
111
109
|
**Show all results (not just violations):**
|
|
112
110
|
```bash
|
|
113
111
|
threadlines check --full
|
|
114
112
|
```
|
|
115
113
|
|
|
114
|
+
**Enable debug logging:**
|
|
115
|
+
```bash
|
|
116
|
+
threadlines check --debug
|
|
117
|
+
```
|
|
118
|
+
|
|
116
119
|
**Options:**
|
|
117
|
-
- `--
|
|
118
|
-
- `--
|
|
119
|
-
- `--
|
|
120
|
-
- `--
|
|
121
|
-
- `--folder <path>` - Review all files in folder recursively
|
|
122
|
-
- `--files <paths...>` - Review multiple specified files
|
|
120
|
+
- `--commit <ref>` - Review specific commit. Accepts commit SHA or git reference (e.g., `HEAD`, `HEAD~1`, `abc123`). Sets review context to `commit`.
|
|
121
|
+
- `--file <path>` - Review entire file (all lines as additions). Sets review context to `file`.
|
|
122
|
+
- `--folder <path>` - Review all files in folder recursively. Sets review context to `folder`.
|
|
123
|
+
- `--files <paths...>` - Review multiple specified files. Sets review context to `files`.
|
|
123
124
|
- `--full` - Show all results (compliant, attention, not_relevant). Default: only attention items
|
|
125
|
+
- `--debug` - Enable debug logging (verbose output for troubleshooting)
|
|
126
|
+
|
|
127
|
+
**Note:** Flags (`--commit`, `--file`, `--folder`, `--files`) are for local development only. In CI/CD environments, these flags are ignored and the CLI auto-detects the appropriate context.
|
|
124
128
|
|
|
125
129
|
**Auto-detection in CI:**
|
|
126
|
-
-
|
|
127
|
-
-
|
|
128
|
-
- Local development →
|
|
130
|
+
- **Pull Request/Merge Request context** → Reviews all changes in the PR/MR (review context: `pr`)
|
|
131
|
+
- **Push to any branch** → Reviews the commit being pushed (review context: `commit`)
|
|
132
|
+
- **Local development** → Reviews staged/unstaged changes (review context: `local`)
|
|
129
133
|
|
|
130
134
|
## Configuration
|
|
131
135
|
|
|
@@ -135,11 +139,40 @@ threadlines check --full
|
|
|
135
139
|
|----------|---------|----------|
|
|
136
140
|
| `THREADLINE_API_KEY` | Authentication with Threadlines API | Yes |
|
|
137
141
|
| `THREADLINE_ACCOUNT` | Your Threadlines account email | Yes |
|
|
138
|
-
| `THREADLINE_API_URL` | Custom API endpoint (default: https://devthreadline.com) | No |
|
|
139
142
|
|
|
140
143
|
Both required variables can be set in a `.env.local` file (recommended for local development) or as environment variables (required for CI/CD).
|
|
141
144
|
|
|
142
|
-
|
|
145
|
+
**Local Development:**
|
|
146
|
+
Create a `.env.local` file in your project root:
|
|
147
|
+
```bash
|
|
148
|
+
THREADLINE_API_KEY=your-api-key-here
|
|
149
|
+
THREADLINE_ACCOUNT=your-email@example.com
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
**CI/CD:**
|
|
153
|
+
Set these as environment variables in your platform:
|
|
154
|
+
- **GitHub Actions**: Settings → Secrets → Add variables
|
|
155
|
+
- **GitLab CI**: Settings → CI/CD → Variables
|
|
156
|
+
- **Bitbucket Pipelines**: Repository settings → Repository variables
|
|
157
|
+
- **Vercel**: Settings → Environment Variables
|
|
158
|
+
|
|
159
|
+
Get your credentials at: https://devthreadline.com/settings
|
|
160
|
+
|
|
161
|
+
### Configuration File (`.threadlinerc`)
|
|
162
|
+
|
|
163
|
+
You can customize the API endpoint and other settings by creating a `.threadlinerc` file in your project root:
|
|
164
|
+
|
|
165
|
+
```json
|
|
166
|
+
{
|
|
167
|
+
"mode": "online",
|
|
168
|
+
"api_url": "https://devthreadline.com",
|
|
169
|
+
"openai_model": "gpt-5.2",
|
|
170
|
+
"openai_service_tier": "Flex",
|
|
171
|
+
"diff_context_lines": 10
|
|
172
|
+
}
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
The `api_url` field allows you to point to a custom server if needed. Default is `https://devthreadline.com`.
|
|
143
176
|
|
|
144
177
|
## Threadline Files
|
|
145
178
|
|
|
@@ -176,26 +209,38 @@ Your guidelines and standards here...
|
|
|
176
209
|
|
|
177
210
|
- **`context_files`**: Array of file paths that provide context (always included, even if unchanged)
|
|
178
211
|
|
|
179
|
-
### Example:
|
|
212
|
+
### Example: Feature Flagging Standards
|
|
180
213
|
|
|
181
214
|
```markdown
|
|
182
215
|
---
|
|
183
|
-
id:
|
|
216
|
+
id: feature-flags
|
|
184
217
|
version: 1.0.0
|
|
185
218
|
patterns:
|
|
186
|
-
- "**/
|
|
187
|
-
- "
|
|
219
|
+
- "**/features/**"
|
|
220
|
+
- "**/components/**"
|
|
221
|
+
- "**/*.tsx"
|
|
222
|
+
- "**/*.ts"
|
|
188
223
|
context_files:
|
|
189
|
-
- "
|
|
224
|
+
- "config/feature-flags.ts"
|
|
190
225
|
---
|
|
191
226
|
|
|
192
|
-
#
|
|
227
|
+
# Feature Flag Standards
|
|
228
|
+
|
|
229
|
+
All feature flag usage must:
|
|
230
|
+
- Check flags using the centralized `isFeatureEnabled()` function from `config/feature-flags.ts`
|
|
231
|
+
- Never hardcode feature flag names as strings (use constants from the config)
|
|
232
|
+
- Include proper cleanup: remove feature flag checks when features are fully rolled out
|
|
233
|
+
- Document rollout plan in PR description (target percentage, timeline)
|
|
234
|
+
- Use feature flags for gradual rollouts, not as permanent configuration
|
|
193
235
|
|
|
194
|
-
|
|
195
|
-
-
|
|
196
|
-
-
|
|
197
|
-
-
|
|
236
|
+
**Violations:**
|
|
237
|
+
- ❌ `if (process.env.NEW_FEATURE === 'true')` (hardcoded, not using registry)
|
|
238
|
+
- ❌ `if (flags['new-feature'])` (string literal instead of constant)
|
|
239
|
+
- ✅ `if (isFeatureEnabled(FeatureFlags.NEW_DASHBOARD))` (using centralized function)
|
|
198
240
|
```
|
|
199
241
|
|
|
200
|
-
The `
|
|
242
|
+
The `config/feature-flags.ts` file will always be included as context, ensuring the AI reviewer can verify that:
|
|
243
|
+
- Feature flag names match the registry
|
|
244
|
+
- The correct flag checking function is used
|
|
245
|
+
- Flags are properly typed and documented
|
|
201
246
|
|
package/dist/api/client.js
CHANGED
|
@@ -19,5 +19,9 @@ class ReviewAPIClient {
|
|
|
19
19
|
const response = await this.client.post('/api/threadline-check', request);
|
|
20
20
|
return response.data;
|
|
21
21
|
}
|
|
22
|
+
async syncResults(request) {
|
|
23
|
+
const response = await this.client.post('/api/threadline-check-results', request);
|
|
24
|
+
return response.data;
|
|
25
|
+
}
|
|
22
26
|
}
|
|
23
27
|
exports.ReviewAPIClient = ReviewAPIClient;
|
package/dist/commands/check.js
CHANGED
|
@@ -46,6 +46,7 @@ const ci_context_1 = require("../git/ci-context");
|
|
|
46
46
|
const local_1 = require("../git/local");
|
|
47
47
|
const config_file_1 = require("../utils/config-file");
|
|
48
48
|
const logger_1 = require("../utils/logger");
|
|
49
|
+
const expert_1 = require("../processors/expert");
|
|
49
50
|
const fs = __importStar(require("fs"));
|
|
50
51
|
const path = __importStar(require("path"));
|
|
51
52
|
const chalk_1 = __importDefault(require("chalk"));
|
|
@@ -78,7 +79,7 @@ async function checkCommand(options) {
|
|
|
78
79
|
const repoRoot = cwd; // Keep for backward compatibility with rest of function
|
|
79
80
|
// Load configuration
|
|
80
81
|
const config = await (0, config_file_1.loadConfig)(cwd);
|
|
81
|
-
|
|
82
|
+
logger_1.logger.info(`🔍 Threadline CLI v${CLI_VERSION}: Checking code against your threadlines...\n`);
|
|
82
83
|
// Get git root for consistent file paths across monorepo
|
|
83
84
|
const git = (0, simple_git_1.default)(cwd);
|
|
84
85
|
let gitRoot;
|
|
@@ -95,51 +96,36 @@ async function checkCommand(options) {
|
|
|
95
96
|
logger_1.logger.error(`Failed to get git root: ${message}`);
|
|
96
97
|
process.exit(1);
|
|
97
98
|
}
|
|
98
|
-
// Pre-flight check: Validate
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
logger_1.logger.
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
console.log(chalk_1.default.gray(' 1. Create a .env.local file in your project root'));
|
|
118
|
-
console.log(chalk_1.default.gray(' 2. Add the missing variable(s):'));
|
|
119
|
-
if (missingVars.includes('THREADLINE_API_KEY')) {
|
|
120
|
-
console.log(chalk_1.default.gray(' THREADLINE_API_KEY=your-api-key-here'));
|
|
121
|
-
}
|
|
122
|
-
if (missingVars.includes('THREADLINE_ACCOUNT')) {
|
|
123
|
-
console.log(chalk_1.default.gray(' THREADLINE_ACCOUNT=your-email@example.com'));
|
|
124
|
-
}
|
|
125
|
-
console.log(chalk_1.default.gray(' 3. Make sure .env.local is in your .gitignore'));
|
|
126
|
-
console.log('');
|
|
127
|
-
console.log(chalk_1.default.white(' CI/CD:'));
|
|
128
|
-
console.log(chalk_1.default.gray(' GitHub Actions: Settings → Secrets → Add variables'));
|
|
129
|
-
console.log(chalk_1.default.gray(' GitLab CI: Settings → CI/CD → Variables'));
|
|
130
|
-
console.log(chalk_1.default.gray(' Bitbucket Pipelines: Repository settings → Repository variables'));
|
|
131
|
-
console.log(chalk_1.default.gray(' Vercel: Settings → Environment Variables'));
|
|
132
|
-
console.log('');
|
|
133
|
-
console.log(chalk_1.default.gray('Get your credentials at: https://devthreadline.com/settings'));
|
|
99
|
+
// Pre-flight check: Validate OpenAI API key is set (required for local processing)
|
|
100
|
+
const openAIConfig = (0, config_1.getOpenAIConfig)(config);
|
|
101
|
+
if (!openAIConfig) {
|
|
102
|
+
logger_1.logger.error('Missing required environment variable: OPENAI_API_KEY');
|
|
103
|
+
logger_1.logger.output('');
|
|
104
|
+
logger_1.logger.output(chalk_1.default.yellow('To fix this:'));
|
|
105
|
+
logger_1.logger.output('');
|
|
106
|
+
logger_1.logger.output(chalk_1.default.white(' Local development:'));
|
|
107
|
+
logger_1.logger.output(chalk_1.default.gray(' 1. Create a .env.local file in your project root'));
|
|
108
|
+
logger_1.logger.output(chalk_1.default.gray(' 2. Add: OPENAI_API_KEY=your-openai-api-key'));
|
|
109
|
+
logger_1.logger.output(chalk_1.default.gray(' 3. Make sure .env.local is in your .gitignore'));
|
|
110
|
+
logger_1.logger.output('');
|
|
111
|
+
logger_1.logger.output(chalk_1.default.white(' CI/CD:'));
|
|
112
|
+
logger_1.logger.output(chalk_1.default.gray(' GitHub Actions: Settings → Secrets → Add OPENAI_API_KEY'));
|
|
113
|
+
logger_1.logger.output(chalk_1.default.gray(' GitLab CI: Settings → CI/CD → Variables → Add OPENAI_API_KEY'));
|
|
114
|
+
logger_1.logger.output(chalk_1.default.gray(' Bitbucket Pipelines: Repository settings → Repository variables → Add OPENAI_API_KEY'));
|
|
115
|
+
logger_1.logger.output(chalk_1.default.gray(' Vercel: Settings → Environment Variables → Add OPENAI_API_KEY'));
|
|
116
|
+
logger_1.logger.output('');
|
|
117
|
+
logger_1.logger.output(chalk_1.default.gray('Get your OpenAI API key at: https://platform.openai.com/api-keys'));
|
|
134
118
|
process.exit(1);
|
|
135
119
|
}
|
|
120
|
+
// Log OpenAI configuration
|
|
121
|
+
(0, config_1.logOpenAIConfig)(openAIConfig);
|
|
136
122
|
// 1. Find and validate threadlines
|
|
137
123
|
logger_1.logger.info('Finding threadlines...');
|
|
138
124
|
const threadlines = await (0, experts_1.findThreadlines)(cwd, gitRoot);
|
|
139
|
-
|
|
125
|
+
logger_1.logger.info(`✓ Found ${threadlines.length} threadline(s)\n`);
|
|
140
126
|
if (threadlines.length === 0) {
|
|
141
|
-
|
|
142
|
-
|
|
127
|
+
logger_1.logger.warn('No valid threadlines found.');
|
|
128
|
+
logger_1.logger.output(chalk_1.default.gray(' Run `npx threadlines init` to create your first threadline.'));
|
|
143
129
|
process.exit(0);
|
|
144
130
|
}
|
|
145
131
|
// 2. Detect environment and context
|
|
@@ -154,7 +140,7 @@ async function checkCommand(options) {
|
|
|
154
140
|
// Validate mutually exclusive flags
|
|
155
141
|
if (explicitFlags.length > 1) {
|
|
156
142
|
logger_1.logger.error('Only one review option can be specified at a time');
|
|
157
|
-
|
|
143
|
+
logger_1.logger.output(chalk_1.default.gray(' Options: --commit, --file, --folder, --files'));
|
|
158
144
|
process.exit(1);
|
|
159
145
|
}
|
|
160
146
|
// CI environments: auto-detect only, flags are ignored with warning
|
|
@@ -224,29 +210,29 @@ async function checkCommand(options) {
|
|
|
224
210
|
}
|
|
225
211
|
}
|
|
226
212
|
if (gitDiff.changedFiles.length === 0) {
|
|
227
|
-
|
|
213
|
+
logger_1.logger.info('ℹ️ No changes detected.');
|
|
228
214
|
process.exit(0);
|
|
229
215
|
}
|
|
230
216
|
// Safety limit: prevent expensive API calls on large diffs
|
|
231
217
|
const MAX_CHANGED_FILES = 20;
|
|
232
218
|
if (gitDiff.changedFiles.length > MAX_CHANGED_FILES) {
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
219
|
+
logger_1.logger.error(`Too many changed files: ${gitDiff.changedFiles.length} (max: ${MAX_CHANGED_FILES})`);
|
|
220
|
+
logger_1.logger.output(chalk_1.default.gray(' This limit prevents expensive API calls on large diffs.'));
|
|
221
|
+
logger_1.logger.output(chalk_1.default.gray(' Consider reviewing smaller batches of changes.'));
|
|
236
222
|
process.exit(1);
|
|
237
223
|
}
|
|
238
224
|
// Check for zero diff (files changed but no actual code changes)
|
|
239
225
|
if (!gitDiff.diff || gitDiff.diff.trim() === '') {
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
226
|
+
logger_1.logger.info('ℹ️ No code changes detected. Diff contains zero lines added or removed.');
|
|
227
|
+
logger_1.logger.output(chalk_1.default.gray(` ${gitDiff.changedFiles.length} file(s) changed but no content modifications detected.`));
|
|
228
|
+
logger_1.logger.output('');
|
|
229
|
+
logger_1.logger.output(chalk_1.default.bold('Results:\n'));
|
|
230
|
+
logger_1.logger.output(chalk_1.default.gray(`${threadlines.length} threadlines checked`));
|
|
231
|
+
logger_1.logger.output(chalk_1.default.gray(` ${threadlines.length} not relevant`));
|
|
232
|
+
logger_1.logger.output('');
|
|
247
233
|
process.exit(0);
|
|
248
234
|
}
|
|
249
|
-
|
|
235
|
+
logger_1.logger.info(`✓ Found ${gitDiff.changedFiles.length} changed file(s) (context: ${reviewContext})\n`);
|
|
250
236
|
// Log the files being sent
|
|
251
237
|
for (const file of gitDiff.changedFiles) {
|
|
252
238
|
logger_1.logger.info(` → ${file}`);
|
|
@@ -281,26 +267,86 @@ async function checkCommand(options) {
|
|
|
281
267
|
contextContent
|
|
282
268
|
};
|
|
283
269
|
});
|
|
284
|
-
// 5.
|
|
270
|
+
// 5. Process threadlines locally using OpenAI
|
|
285
271
|
logger_1.logger.info('Running threadline checks...');
|
|
286
|
-
const
|
|
287
|
-
|
|
288
|
-
|
|
272
|
+
const processResponse = await (0, expert_1.processThreadlines)({
|
|
273
|
+
threadlines: threadlinesWithContext.map(t => ({
|
|
274
|
+
id: t.id,
|
|
275
|
+
version: t.version,
|
|
276
|
+
patterns: t.patterns,
|
|
277
|
+
content: t.content,
|
|
278
|
+
contextFiles: t.contextFiles,
|
|
279
|
+
contextContent: t.contextContent
|
|
280
|
+
})),
|
|
289
281
|
diff: gitDiff.diff,
|
|
290
282
|
files: gitDiff.changedFiles,
|
|
291
|
-
apiKey: apiKey,
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
commitSha: metadata.commitSha,
|
|
296
|
-
commitMessage: metadata.commitMessage,
|
|
297
|
-
commitAuthorName: metadata.commitAuthorName,
|
|
298
|
-
commitAuthorEmail: metadata.commitAuthorEmail,
|
|
299
|
-
prTitle: metadata.prTitle,
|
|
300
|
-
environment: environment,
|
|
301
|
-
cliVersion: CLI_VERSION,
|
|
302
|
-
reviewContext: reviewContext
|
|
283
|
+
apiKey: openAIConfig.apiKey,
|
|
284
|
+
model: openAIConfig.model,
|
|
285
|
+
serviceTier: openAIConfig.serviceTier,
|
|
286
|
+
contextLinesForLLM: config.diff_context_lines
|
|
303
287
|
});
|
|
288
|
+
// Convert ProcessThreadlinesResponse to ReviewResponse format for displayResults
|
|
289
|
+
const response = {
|
|
290
|
+
results: processResponse.results,
|
|
291
|
+
metadata: {
|
|
292
|
+
totalThreadlines: processResponse.metadata.totalThreadlines,
|
|
293
|
+
completed: processResponse.metadata.completed,
|
|
294
|
+
timedOut: processResponse.metadata.timedOut,
|
|
295
|
+
errors: processResponse.metadata.errors
|
|
296
|
+
}
|
|
297
|
+
};
|
|
298
|
+
// 6. Sync results to web app (if mode is "online")
|
|
299
|
+
if (config.mode === 'online') {
|
|
300
|
+
const apiKey = (0, config_1.getThreadlineApiKey)();
|
|
301
|
+
const account = (0, config_1.getThreadlineAccount)();
|
|
302
|
+
if (!apiKey || !account) {
|
|
303
|
+
// Configuration error: mode is "online" but credentials are missing
|
|
304
|
+
// Fail loudly - this is a user configuration error that needs to be fixed
|
|
305
|
+
logger_1.logger.error('Sync mode is "online" but required credentials are missing.');
|
|
306
|
+
logger_1.logger.error('Set THREADLINE_API_KEY and THREADLINE_ACCOUNT environment variables to enable syncing.');
|
|
307
|
+
logger_1.logger.error('Alternatively, set mode to "offline" in .threadlinerc to disable syncing.');
|
|
308
|
+
throw new Error('Sync configuration error: mode is "online" but THREADLINE_API_KEY or THREADLINE_ACCOUNT is not set. ' +
|
|
309
|
+
'Either set these environment variables or change mode to "offline" in .threadlinerc.');
|
|
310
|
+
}
|
|
311
|
+
// Attempt sync - if it fails, show error but don't fail the check (local processing succeeded)
|
|
312
|
+
try {
|
|
313
|
+
logger_1.logger.info('Syncing results to web app...');
|
|
314
|
+
const client = new client_1.ReviewAPIClient(config.api_url);
|
|
315
|
+
await client.syncResults({
|
|
316
|
+
threadlines: threadlinesWithContext,
|
|
317
|
+
diff: gitDiff.diff,
|
|
318
|
+
files: gitDiff.changedFiles,
|
|
319
|
+
results: processResponse.results,
|
|
320
|
+
metadata: {
|
|
321
|
+
totalThreadlines: processResponse.metadata.totalThreadlines,
|
|
322
|
+
completed: processResponse.metadata.completed,
|
|
323
|
+
timedOut: processResponse.metadata.timedOut,
|
|
324
|
+
errors: processResponse.metadata.errors,
|
|
325
|
+
llmModel: processResponse.metadata.llmModel
|
|
326
|
+
},
|
|
327
|
+
apiKey,
|
|
328
|
+
account,
|
|
329
|
+
repoName,
|
|
330
|
+
branchName,
|
|
331
|
+
commitSha: metadata.commitSha,
|
|
332
|
+
commitMessage: metadata.commitMessage,
|
|
333
|
+
commitAuthorName: metadata.commitAuthorName,
|
|
334
|
+
commitAuthorEmail: metadata.commitAuthorEmail,
|
|
335
|
+
prTitle: metadata.prTitle,
|
|
336
|
+
environment: environment,
|
|
337
|
+
cliVersion: CLI_VERSION,
|
|
338
|
+
reviewContext: reviewContext
|
|
339
|
+
});
|
|
340
|
+
logger_1.logger.info('✓ Results synced successfully');
|
|
341
|
+
}
|
|
342
|
+
catch (error) {
|
|
343
|
+
// Sync API call failed - show error prominently but don't fail the check (local processing succeeded)
|
|
344
|
+
// This is not a silent fallback: we explicitly show the error and explain why we continue
|
|
345
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
346
|
+
logger_1.logger.error(`Failed to sync results to web app: ${errorMessage}`);
|
|
347
|
+
logger_1.logger.warn('Check results are still valid - sync failure does not affect local processing.');
|
|
348
|
+
}
|
|
349
|
+
}
|
|
304
350
|
// 7. Display results (with filtering if --full not specified)
|
|
305
351
|
displayResults(response, options.full || false);
|
|
306
352
|
// Exit with appropriate code (attention or errors = failure)
|
|
@@ -315,7 +361,7 @@ function displayResults(response, showFull) {
|
|
|
315
361
|
: results.filter((r) => r.status === 'attention');
|
|
316
362
|
// Display informational message if present (e.g., zero diffs)
|
|
317
363
|
if (message) {
|
|
318
|
-
|
|
364
|
+
logger_1.logger.output('\n' + chalk_1.default.blue('ℹ️ ' + message));
|
|
319
365
|
}
|
|
320
366
|
const notRelevant = results.filter((r) => r.status === 'not_relevant').length;
|
|
321
367
|
const compliant = results.filter((r) => r.status === 'compliant').length;
|
|
@@ -344,81 +390,81 @@ function displayResults(response, showFull) {
|
|
|
344
390
|
// Show success message with breakdown if no issues
|
|
345
391
|
if (attention === 0 && metadata.timedOut === 0 && errors === 0) {
|
|
346
392
|
const summary = summaryParts.length > 0 ? ` (${summaryParts.join(', ')})` : '';
|
|
347
|
-
|
|
348
|
-
|
|
393
|
+
logger_1.logger.output('\n' + chalk_1.default.green(`✓ Threadline check passed${summary}`));
|
|
394
|
+
logger_1.logger.output(chalk_1.default.gray(` ${metadata.totalThreadlines} threadline${metadata.totalThreadlines !== 1 ? 's' : ''} checked\n`));
|
|
349
395
|
}
|
|
350
396
|
else {
|
|
351
397
|
// Show detailed breakdown when there are issues
|
|
352
|
-
|
|
353
|
-
|
|
398
|
+
logger_1.logger.output('\n' + chalk_1.default.bold('Results:\n'));
|
|
399
|
+
logger_1.logger.output(chalk_1.default.gray(`${metadata.totalThreadlines} threadlines checked`));
|
|
354
400
|
if (showFull) {
|
|
355
401
|
// Show all results when --full flag is used
|
|
356
402
|
if (notRelevant > 0) {
|
|
357
|
-
|
|
403
|
+
logger_1.logger.output(chalk_1.default.gray(` ${notRelevant} not relevant`));
|
|
358
404
|
}
|
|
359
405
|
if (compliant > 0) {
|
|
360
|
-
|
|
406
|
+
logger_1.logger.output(chalk_1.default.green(` ${compliant} compliant`));
|
|
361
407
|
}
|
|
362
408
|
if (attention > 0) {
|
|
363
|
-
|
|
409
|
+
logger_1.logger.output(chalk_1.default.yellow(` ${attention} attention`));
|
|
364
410
|
}
|
|
365
411
|
}
|
|
366
412
|
else {
|
|
367
413
|
// Default: only show attention items
|
|
368
414
|
if (attention > 0) {
|
|
369
|
-
|
|
415
|
+
logger_1.logger.output(chalk_1.default.yellow(` ${attention} attention`));
|
|
370
416
|
}
|
|
371
417
|
}
|
|
372
418
|
if (metadata.timedOut > 0) {
|
|
373
|
-
|
|
419
|
+
logger_1.logger.output(chalk_1.default.yellow(` ${metadata.timedOut} timed out`));
|
|
374
420
|
}
|
|
375
421
|
if (errors > 0) {
|
|
376
|
-
|
|
422
|
+
logger_1.logger.output(chalk_1.default.red(` ${errors} errors`));
|
|
377
423
|
}
|
|
378
|
-
|
|
424
|
+
logger_1.logger.output('');
|
|
379
425
|
}
|
|
380
426
|
// Show attention items
|
|
381
427
|
if (attentionItems.length > 0) {
|
|
382
428
|
for (const item of attentionItems) {
|
|
383
|
-
|
|
429
|
+
logger_1.logger.output(chalk_1.default.yellow(`[attention] ${item.expertId}`));
|
|
384
430
|
if (item.fileReferences && item.fileReferences.length > 0) {
|
|
385
431
|
// List all files as bullet points
|
|
386
432
|
for (const fileRef of item.fileReferences) {
|
|
387
433
|
const lineRef = item.lineReferences?.[item.fileReferences.indexOf(fileRef)];
|
|
388
434
|
const lineStr = lineRef ? `:${lineRef}` : '';
|
|
389
|
-
|
|
435
|
+
logger_1.logger.output(chalk_1.default.gray(`* ${fileRef}${lineStr}`));
|
|
390
436
|
}
|
|
391
437
|
}
|
|
392
438
|
// Show reasoning once at the end (if available)
|
|
393
439
|
if (item.reasoning) {
|
|
394
|
-
|
|
440
|
+
logger_1.logger.output(chalk_1.default.gray(item.reasoning));
|
|
395
441
|
}
|
|
396
442
|
else if (!item.fileReferences || item.fileReferences.length === 0) {
|
|
397
|
-
|
|
443
|
+
logger_1.logger.output(chalk_1.default.gray('Needs attention'));
|
|
398
444
|
}
|
|
399
|
-
|
|
445
|
+
logger_1.logger.output(''); // Empty line between threadlines
|
|
400
446
|
}
|
|
401
447
|
}
|
|
402
448
|
// Show error items (always shown, regardless of --full flag)
|
|
403
449
|
if (errorItems.length > 0) {
|
|
404
450
|
for (const item of errorItems) {
|
|
405
|
-
|
|
451
|
+
logger_1.logger.output(chalk_1.default.red(`[error] ${item.expertId}`));
|
|
406
452
|
// Show error message
|
|
407
453
|
if (item.error) {
|
|
408
|
-
|
|
454
|
+
logger_1.logger.output(chalk_1.default.red(` Error: ${item.error.message}`));
|
|
409
455
|
if (item.error.type) {
|
|
410
|
-
|
|
456
|
+
logger_1.logger.output(chalk_1.default.red(` Type: ${item.error.type}`));
|
|
411
457
|
}
|
|
412
458
|
if (item.error.code) {
|
|
413
|
-
|
|
459
|
+
logger_1.logger.output(chalk_1.default.red(` Code: ${item.error.code}`));
|
|
414
460
|
}
|
|
415
461
|
// Show raw response for debugging
|
|
416
462
|
if (item.error.rawResponse) {
|
|
417
|
-
|
|
418
|
-
|
|
463
|
+
logger_1.logger.output(chalk_1.default.gray(' Raw response:'));
|
|
464
|
+
logger_1.logger.output(chalk_1.default.gray(JSON.stringify(item.error.rawResponse, null, 2).split('\n').map(line => ' ' + line).join('\n')));
|
|
419
465
|
}
|
|
420
466
|
}
|
|
421
|
-
|
|
467
|
+
logger_1.logger.output(''); // Empty line between errors
|
|
422
468
|
}
|
|
423
469
|
}
|
|
424
470
|
}
|