threadlines 0.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.
package/README.md ADDED
@@ -0,0 +1,31 @@
1
+ # threadlines
2
+
3
+ Threadline CLI - AI-powered linter based on your natural language documentation.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install -g threadlines
9
+ ```
10
+
11
+ Or use with npx:
12
+
13
+ ```bash
14
+ npx threadlines check
15
+ ```
16
+
17
+ ## Usage
18
+
19
+ ```bash
20
+ threadlines check
21
+ ```
22
+
23
+ ## Configuration
24
+
25
+ - `THREADLINE_API_URL` - Server URL (default: http://localhost:3000)
26
+ - `OPENAI_API_KEY` - Your OpenAI API key (required)
27
+
28
+ ## Expert Files
29
+
30
+ Create a `/threadlines` folder in your repository with markdown files. See [Threadline Format](../../docs/EXPERT_FORMAT.md) for details.
31
+
package/bin/threadline ADDED
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env node
2
+
3
+ require('../dist/index.js');
4
+
@@ -0,0 +1,36 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.ReviewAPIClient = void 0;
7
+ const axios_1 = __importDefault(require("axios"));
8
+ class ReviewAPIClient {
9
+ constructor(baseURL) {
10
+ this.client = axios_1.default.create({
11
+ baseURL,
12
+ timeout: 60000, // 60s timeout for entire request
13
+ headers: {
14
+ 'Content-Type': 'application/json'
15
+ }
16
+ });
17
+ }
18
+ async review(request) {
19
+ try {
20
+ const response = await this.client.post('/api/threadline-check', request);
21
+ return response.data;
22
+ }
23
+ catch (error) {
24
+ if (error.response) {
25
+ throw new Error(`API error: ${error.response.status} - ${error.response.data?.message || error.message}`);
26
+ }
27
+ else if (error.request) {
28
+ throw new Error(`Network error: Could not reach Threadline server at ${this.client.defaults.baseURL}`);
29
+ }
30
+ else {
31
+ throw new Error(`Request error: ${error.message}`);
32
+ }
33
+ }
34
+ }
35
+ }
36
+ exports.ReviewAPIClient = ReviewAPIClient;
@@ -0,0 +1,157 @@
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
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.checkCommand = checkCommand;
40
+ const experts_1 = require("../validators/experts");
41
+ const diff_1 = require("../git/diff");
42
+ const client_1 = require("../api/client");
43
+ const fs = __importStar(require("fs"));
44
+ const path = __importStar(require("path"));
45
+ const chalk_1 = __importDefault(require("chalk"));
46
+ async function checkCommand(options) {
47
+ const repoRoot = process.cwd();
48
+ console.log(chalk_1.default.blue('🔍 Threadline: Checking code against your threadlines...\n'));
49
+ try {
50
+ // 1. Find and validate threadlines
51
+ console.log(chalk_1.default.gray('📋 Finding threadlines...'));
52
+ const threadlines = await (0, experts_1.findThreadlines)(repoRoot);
53
+ console.log(chalk_1.default.green(`✓ Found ${threadlines.length} threadline(s)\n`));
54
+ if (threadlines.length === 0) {
55
+ console.log(chalk_1.default.yellow('⚠️ No valid threadlines found. Add threadline files to /threadlines folder.'));
56
+ process.exit(0);
57
+ }
58
+ // 2. Get git diff
59
+ console.log(chalk_1.default.gray('📝 Collecting git changes...'));
60
+ const gitDiff = await (0, diff_1.getGitDiff)(repoRoot);
61
+ if (gitDiff.changedFiles.length === 0) {
62
+ console.log(chalk_1.default.yellow('⚠️ No changes detected. Make some code changes and try again.'));
63
+ process.exit(0);
64
+ }
65
+ console.log(chalk_1.default.green(`✓ Found ${gitDiff.changedFiles.length} changed file(s)\n`));
66
+ // 3. Read context files for each threadline
67
+ const threadlinesWithContext = threadlines.map(threadline => {
68
+ const contextContent = {};
69
+ if (threadline.contextFiles) {
70
+ for (const contextFile of threadline.contextFiles) {
71
+ const fullPath = path.join(repoRoot, contextFile);
72
+ if (fs.existsSync(fullPath)) {
73
+ contextContent[contextFile] = fs.readFileSync(fullPath, 'utf-8');
74
+ }
75
+ }
76
+ }
77
+ return {
78
+ id: threadline.id,
79
+ version: threadline.version,
80
+ patterns: threadline.patterns,
81
+ content: threadline.content,
82
+ contextFiles: threadline.contextFiles,
83
+ contextContent
84
+ };
85
+ });
86
+ // 4. Get API URL and key
87
+ const apiUrl = options.apiUrl || process.env.THREADLINE_API_URL || 'http://localhost:3000';
88
+ const apiKey = options.apiKey || process.env.OPENAI_API_KEY;
89
+ if (!apiKey) {
90
+ throw new Error('OpenAI API key required. Set OPENAI_API_KEY environment variable.');
91
+ }
92
+ // 5. Call review API
93
+ console.log(chalk_1.default.gray('🤖 Running threadline checks...'));
94
+ const client = new client_1.ReviewAPIClient(apiUrl);
95
+ const response = await client.review({
96
+ threadlines: threadlinesWithContext,
97
+ diff: gitDiff.diff,
98
+ files: gitDiff.changedFiles,
99
+ apiKey
100
+ });
101
+ // 6. Display results
102
+ displayResults(response);
103
+ // Exit with appropriate code
104
+ const hasAttention = response.results.some(r => r.status === 'attention');
105
+ process.exit(hasAttention ? 1 : 0);
106
+ }
107
+ catch (error) {
108
+ console.error(chalk_1.default.red(`\n❌ Error: ${error.message}`));
109
+ process.exit(1);
110
+ }
111
+ }
112
+ function displayResults(response) {
113
+ const { results, metadata } = response;
114
+ console.log('\n' + chalk_1.default.bold('Results:\n'));
115
+ console.log(chalk_1.default.gray(`${metadata.totalThreadlines} threadlines checked`));
116
+ const notRelevant = results.filter((r) => r.status === 'not_relevant').length;
117
+ const compliant = results.filter((r) => r.status === 'compliant').length;
118
+ const attention = results.filter((r) => r.status === 'attention').length;
119
+ if (notRelevant > 0) {
120
+ console.log(chalk_1.default.gray(` ${notRelevant} not relevant`));
121
+ }
122
+ if (compliant > 0) {
123
+ console.log(chalk_1.default.green(` ${compliant} compliant`));
124
+ }
125
+ if (attention > 0) {
126
+ console.log(chalk_1.default.yellow(` ${attention} attention`));
127
+ }
128
+ if (metadata.timedOut > 0) {
129
+ console.log(chalk_1.default.yellow(` ${metadata.timedOut} timed out`));
130
+ }
131
+ if (metadata.errors > 0) {
132
+ console.log(chalk_1.default.red(` ${metadata.errors} errors`));
133
+ }
134
+ console.log('');
135
+ // Show attention items
136
+ const attentionItems = results.filter((r) => r.status === 'attention');
137
+ if (attentionItems.length > 0) {
138
+ for (const item of attentionItems) {
139
+ console.log(chalk_1.default.yellow(`⚠️ ${item.expertId}`));
140
+ if (item.fileReferences && item.fileReferences.length > 0) {
141
+ for (const fileRef of item.fileReferences) {
142
+ const lineRef = item.lineReferences?.[item.fileReferences.indexOf(fileRef)];
143
+ const lineStr = lineRef ? `:${lineRef}` : '';
144
+ console.log(chalk_1.default.gray(` ${fileRef}${lineStr} - ${item.reasoning || 'Needs attention'}`));
145
+ }
146
+ }
147
+ else if (item.reasoning) {
148
+ console.log(chalk_1.default.gray(` ${item.reasoning}`));
149
+ }
150
+ }
151
+ console.log('');
152
+ }
153
+ // Show compliant items (optional, can be verbose)
154
+ if (attentionItems.length === 0 && compliant > 0) {
155
+ console.log(chalk_1.default.green('✓ All threadlines passed!\n'));
156
+ }
157
+ }
@@ -0,0 +1,41 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.getGitDiff = getGitDiff;
7
+ const simple_git_1 = __importDefault(require("simple-git"));
8
+ async function getGitDiff(repoRoot) {
9
+ const git = (0, simple_git_1.default)(repoRoot);
10
+ // Check if we're in a git repo
11
+ const isRepo = await git.checkIsRepo();
12
+ if (!isRepo) {
13
+ throw new Error('Not a git repository. Threadline requires a git repository.');
14
+ }
15
+ // Get diff (staged changes, or unstaged if no staged)
16
+ const status = await git.status();
17
+ let diff;
18
+ if (status.staged.length > 0) {
19
+ // Use staged changes
20
+ diff = await git.diff(['--cached']);
21
+ }
22
+ else if (status.files.length > 0) {
23
+ // Use unstaged changes
24
+ diff = await git.diff();
25
+ }
26
+ else {
27
+ // No changes
28
+ return {
29
+ diff: '',
30
+ changedFiles: []
31
+ };
32
+ }
33
+ // Get list of changed files
34
+ const changedFiles = status.files
35
+ .filter(f => f.working_dir !== ' ' || f.index !== ' ')
36
+ .map(f => f.path);
37
+ return {
38
+ diff: diff || '',
39
+ changedFiles
40
+ };
41
+ }
package/dist/index.js ADDED
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ const commander_1 = require("commander");
5
+ const check_1 = require("./commands/check");
6
+ const program = new commander_1.Command();
7
+ program
8
+ .name('threadline')
9
+ .description('AI-powered linter based on your natural language documentation')
10
+ .version('0.1.0');
11
+ program
12
+ .command('check')
13
+ .description('Check code against your experts')
14
+ .option('--api-url <url>', 'Threadline server URL', process.env.THREADLINE_API_URL || 'http://localhost:3000')
15
+ .option('--api-key <key>', 'OpenAI API key', process.env.OPENAI_API_KEY)
16
+ .action(check_1.checkCommand);
17
+ program.parse();
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,133 @@
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
+ exports.findThreadlines = findThreadlines;
37
+ exports.validateThreadline = validateThreadline;
38
+ const fs = __importStar(require("fs"));
39
+ const path = __importStar(require("path"));
40
+ const yaml = __importStar(require("js-yaml"));
41
+ const REQUIRED_FIELDS = ['id', 'version', 'patterns'];
42
+ async function findThreadlines(repoRoot) {
43
+ const expertsDir = path.join(repoRoot, 'threadlines');
44
+ if (!fs.existsSync(expertsDir)) {
45
+ throw new Error('No /threadlines folder found. Create a /threadlines folder with your threadline markdown files.');
46
+ }
47
+ const files = fs.readdirSync(expertsDir);
48
+ const expertFiles = files.filter(f => f.endsWith('.md'));
49
+ if (expertFiles.length === 0) {
50
+ throw new Error('No threadline files found in /threadlines folder. Add .md files with threadline definitions.');
51
+ }
52
+ const threadlines = [];
53
+ for (const file of expertFiles) {
54
+ const result = await validateThreadline(path.join(expertsDir, file), repoRoot);
55
+ if (result.valid && result.threadline) {
56
+ threadlines.push(result.threadline);
57
+ }
58
+ else {
59
+ console.warn(`⚠️ Skipping ${file}: ${result.errors?.join(', ')}`);
60
+ }
61
+ }
62
+ return threadlines;
63
+ }
64
+ async function validateThreadline(filePath, repoRoot) {
65
+ try {
66
+ const content = fs.readFileSync(filePath, 'utf-8');
67
+ // Parse YAML frontmatter
68
+ const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
69
+ if (!frontmatterMatch) {
70
+ return {
71
+ valid: false,
72
+ errors: ['Missing YAML frontmatter. Threadline files must start with ---']
73
+ };
74
+ }
75
+ const frontmatter = yaml.load(frontmatterMatch[1]);
76
+ const body = frontmatterMatch[2].trim();
77
+ // Validate required fields
78
+ const errors = [];
79
+ for (const field of REQUIRED_FIELDS) {
80
+ if (!frontmatter[field]) {
81
+ errors.push(`Missing required field: ${field}`);
82
+ }
83
+ }
84
+ // Validate patterns
85
+ if (frontmatter.patterns && !Array.isArray(frontmatter.patterns)) {
86
+ errors.push('patterns must be an array');
87
+ }
88
+ if (frontmatter.patterns && frontmatter.patterns.length === 0) {
89
+ errors.push('patterns array cannot be empty');
90
+ }
91
+ // Validate context_files if present
92
+ if (frontmatter.context_files) {
93
+ if (!Array.isArray(frontmatter.context_files)) {
94
+ errors.push('context_files must be an array');
95
+ }
96
+ else {
97
+ // Check if context files exist
98
+ for (const contextFile of frontmatter.context_files) {
99
+ const fullPath = path.join(repoRoot, contextFile);
100
+ if (!fs.existsSync(fullPath)) {
101
+ errors.push(`Context file not found: ${contextFile}`);
102
+ }
103
+ }
104
+ }
105
+ }
106
+ // Validate body has content
107
+ if (!body || body.length === 0) {
108
+ errors.push('Threadline body cannot be empty');
109
+ }
110
+ // Validate version format (basic semver check)
111
+ if (frontmatter.version && !/^\d+\.\d+\.\d+/.test(frontmatter.version)) {
112
+ errors.push('version must be in semver format (e.g., 1.0.0)');
113
+ }
114
+ if (errors.length > 0) {
115
+ return { valid: false, errors };
116
+ }
117
+ const threadline = {
118
+ id: frontmatter.id,
119
+ version: frontmatter.version,
120
+ patterns: frontmatter.patterns,
121
+ contextFiles: frontmatter.context_files || [],
122
+ content: body,
123
+ filePath: path.relative(repoRoot, filePath)
124
+ };
125
+ return { valid: true, threadline };
126
+ }
127
+ catch (error) {
128
+ return {
129
+ valid: false,
130
+ errors: [`Failed to parse threadline file: ${error.message}`]
131
+ };
132
+ }
133
+ }
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "threadlines",
3
+ "version": "0.1.0",
4
+ "description": "Threadline CLI - AI-powered linter based on your natural language documentation",
5
+ "main": "dist/index.js",
6
+ "bin": {
7
+ "threadline": "./bin/threadline"
8
+ },
9
+ "files": [
10
+ "dist",
11
+ "bin",
12
+ "README.md"
13
+ ],
14
+ "keywords": [
15
+ "code-quality",
16
+ "linter",
17
+ "ai",
18
+ "code-review",
19
+ "threadline",
20
+ "code-standards"
21
+ ],
22
+ "author": "",
23
+ "license": "MIT",
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "https://github.com/ngrootscholten/threadline.git",
27
+ "directory": "packages/cli"
28
+ },
29
+ "scripts": {
30
+ "build": "tsc",
31
+ "dev": "tsc --watch",
32
+ "start": "node dist/index.js",
33
+ "prepublishOnly": "npm run build"
34
+ },
35
+ "dependencies": {
36
+ "commander": "^12.1.0",
37
+ "simple-git": "^3.27.0",
38
+ "axios": "^1.7.9",
39
+ "chalk": "^4.1.2",
40
+ "js-yaml": "^4.1.0"
41
+ },
42
+ "devDependencies": {
43
+ "@types/node": "^22.10.2",
44
+ "@types/js-yaml": "^4.0.9",
45
+ "typescript": "^5.7.2"
46
+ }
47
+ }
48
+