linear-ai-build 1.0.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.
Files changed (41) hide show
  1. package/.env.example +3 -0
  2. package/README.md +220 -0
  3. package/bin/cli.js +21 -0
  4. package/bin/cli.ts +51 -0
  5. package/bin/init.ts +252 -0
  6. package/commands/build-from-linear.md +193 -0
  7. package/commands/generate-stories.md +110 -0
  8. package/dist/bridge/config.d.ts +11 -0
  9. package/dist/bridge/config.js +24 -0
  10. package/dist/bridge/executor.d.ts +8 -0
  11. package/dist/bridge/executor.js +147 -0
  12. package/dist/bridge/index.d.ts +3 -0
  13. package/dist/bridge/index.js +137 -0
  14. package/dist/bridge/linear-helpers.d.ts +6 -0
  15. package/dist/bridge/linear-helpers.js +43 -0
  16. package/dist/bridge/reporter.d.ts +7 -0
  17. package/dist/bridge/reporter.js +79 -0
  18. package/dist/bridge/state.d.ts +10 -0
  19. package/dist/bridge/state.js +37 -0
  20. package/dist/bridge/transformer.d.ts +22 -0
  21. package/dist/bridge/transformer.js +142 -0
  22. package/dist/bridge/types.d.ts +44 -0
  23. package/dist/bridge/types.js +1 -0
  24. package/dist/bridge/watcher.d.ts +13 -0
  25. package/dist/bridge/watcher.js +104 -0
  26. package/dist/generate-sdk.d.ts +1 -0
  27. package/dist/generate-sdk.js +471 -0
  28. package/dist/index.d.ts +33 -0
  29. package/dist/index.js +232 -0
  30. package/package.json +61 -0
  31. package/src/bridge/config.ts +39 -0
  32. package/src/bridge/executor.ts +167 -0
  33. package/src/bridge/index.ts +156 -0
  34. package/src/bridge/linear-helpers.ts +49 -0
  35. package/src/bridge/reporter.ts +93 -0
  36. package/src/bridge/state.ts +50 -0
  37. package/src/bridge/transformer.ts +173 -0
  38. package/src/bridge/types.ts +45 -0
  39. package/src/bridge/watcher.ts +122 -0
  40. package/src/generate-sdk.ts +570 -0
  41. package/src/index.ts +287 -0
package/.env.example ADDED
@@ -0,0 +1,3 @@
1
+ ANTHROPIC_API_KEY=sk-ant-your-key-here
2
+ LINEAR_API_KEY=lin_api_your-key-here
3
+ LINEAR_TEAM_ID=your-team-id-here
package/README.md ADDED
@@ -0,0 +1,220 @@
1
+ # Linear AI Build
2
+
3
+ Generate user stories from feature descriptions, push them to Linear, then have AI agents build the code — all from Claude Code.
4
+
5
+ ## What It Does
6
+
7
+ 1. **Generates stories** — Asks clarifying questions, creates a PRD with user stories, acceptance criteria, and tasks
8
+ 2. **Pushes to Linear** — Creates a project with parent issues (stories) and child issues (tasks), labels, and priorities
9
+ 3. **Builds from Linear** — Pulls labeled stories back from Linear and orchestrates Claude Flow agents to plan, implement, test, and review the code
10
+
11
+ ## Quick Start
12
+
13
+ ### Install
14
+
15
+ ```bash
16
+ git clone <your-repo>
17
+ cd linear-ai-build
18
+ npm install
19
+ ```
20
+
21
+ ### Configure
22
+
23
+ ```bash
24
+ npm run init
25
+ ```
26
+
27
+ This will:
28
+ 1. Ask for your **Linear API key** (from Linear Settings > Security & Access > Personal API keys)
29
+ 2. Ask for your **Anthropic API key** (optional — only needed for standalone CLI)
30
+ 3. Ask for your **default Linear team ID** (optional)
31
+ 4. Write a `.env` file with your credentials
32
+ 5. Configure the **Linear MCP server** for Claude Code (`.mcp.json`)
33
+ 6. Install the **Claude Code commands**
34
+
35
+ ### Prerequisites for Building
36
+
37
+ Install Claude Flow globally:
38
+
39
+ ```bash
40
+ npm install -g claude-flow@alpha
41
+ ```
42
+
43
+ ## Usage
44
+
45
+ ### Generate Stories
46
+
47
+ Create stories and tasks in Linear from a feature description.
48
+
49
+ **CLI:**
50
+
51
+ ```bash
52
+ npm run generate -- "Add user authentication with OAuth" TEAM-abc123
53
+ ```
54
+
55
+ **Claude Code:**
56
+
57
+ ```
58
+ /linear:generate-stories "Add user authentication with OAuth" TEAM-abc123
59
+ ```
60
+
61
+ **Options:**
62
+
63
+ | Flag | Description |
64
+ |---|---|
65
+ | `--dry-run` | Preview the generated PRD without creating anything in Linear |
66
+ | `--cycle <id>` | Assign issues to a specific cycle (defaults to active cycle) |
67
+ | `--assignee <id>` | Assign issues to a specific user (defaults to you) |
68
+ | `--no-cycle` | Skip cycle assignment |
69
+ | `--no-assignee` | Skip assignee assignment |
70
+ | `--skip-questions` | Skip clarifying questions, generate PRD directly |
71
+
72
+ ### Build From Linear
73
+
74
+ Pull labeled stories from Linear and have Claude Flow agents implement the code.
75
+
76
+ **Claude Code:**
77
+
78
+ ```
79
+ /linear:build-from-linear
80
+ /linear:build-from-linear WIC-42
81
+ /linear:build-from-linear --label ai-build
82
+ ```
83
+
84
+ This command:
85
+ 1. Uses **Linear MCP** to fetch stories with the `ai-build` label (or a specific issue)
86
+ 2. Fetches child tasks and acceptance criteria for each story
87
+ 3. Asks you to confirm which issues to process
88
+ 4. Uses **Claude Flow MCP** to orchestrate agents:
89
+ - **Planner** — analyzes the story and creates an implementation plan
90
+ - **Implementers** — build each task in parallel
91
+ - **Tester** — writes and runs tests against acceptance criteria
92
+ - **Reviewer** — reviews all changes, fixes issues
93
+ 5. Creates a git branch per story (`linear/<issue-identifier>`)
94
+ 6. Updates Linear with progress comments and moves issues to "In Review"
95
+
96
+ ### Watch Mode (CLI)
97
+
98
+ Alternatively, poll Linear and auto-process issues without Claude Code:
99
+
100
+ ```bash
101
+ npm run watch
102
+ ```
103
+
104
+ Or process a single issue:
105
+
106
+ ```bash
107
+ npm run build:issue -- WIC-42
108
+ ```
109
+
110
+ ## How It Works
111
+
112
+ ```
113
+ Feature Description
114
+ |
115
+ v
116
+ /linear:generate-stories
117
+ |
118
+ v
119
+ Linear (stories + tasks created)
120
+ |
121
+ Label with "ai-build"
122
+ |
123
+ v
124
+ /linear:build-from-linear
125
+ |
126
+ v
127
+ Claude Flow Agents
128
+ Plan → Implement → Test → Review
129
+ |
130
+ v
131
+ Git branch + PR ready
132
+ Linear issues moved to "In Review"
133
+ ```
134
+
135
+ ## Environment Variables
136
+
137
+ | Variable | Required | Description |
138
+ |---|---|---|
139
+ | `LINEAR_API_KEY` | Yes | Linear API key |
140
+ | `LINEAR_TEAM_ID` | Yes | Default team ID or key (e.g., `WIC`) |
141
+ | `ANTHROPIC_API_KEY` | For CLI | Anthropic API key (not needed for Claude Code plugin) |
142
+ | `TARGET_REPO_PATH` | No | Where agents write code (default: current directory) |
143
+ | `TARGET_BRANCH` | No | Base branch for new branches (default: `main`) |
144
+ | `POLL_INTERVAL_MS` | No | Watch mode polling interval (default: `30000`) |
145
+ | `AI_BUILD_LABEL` | No | Linear label to watch for (default: `ai-build`) |
146
+
147
+ ## MCP Servers
148
+
149
+ Configured in `.mcp.json`:
150
+
151
+ | Server | Purpose |
152
+ |---|---|
153
+ | `linear` | Linear API — fetch/create/update issues, comments, projects |
154
+ | `claude-flow` | Claude Flow — agent swarm orchestration, task management |
155
+
156
+ ## Project Structure
157
+
158
+ ```
159
+ linear-ai-build/
160
+ bin/
161
+ cli.ts # CLI entry point (init, generate, watch, build)
162
+ init.ts # Interactive setup
163
+ commands/
164
+ generate-stories.md # /linear:generate-stories command
165
+ build-from-linear.md # /linear:build-from-linear command
166
+ src/
167
+ generate-sdk.ts # PRD generation + Linear SDK integration
168
+ index.ts # MCP-based alternative
169
+ bridge/
170
+ index.ts # Watch/build entry point
171
+ watcher.ts # Linear polling loop
172
+ executor.ts # Claude Flow workflow executor
173
+ transformer.ts # Issue → workflow builder
174
+ reporter.ts # Linear status updates
175
+ linear-helpers.ts # Shared Linear SDK helpers
176
+ config.ts # Bridge configuration
177
+ state.ts # Deduplication tracking
178
+ types.ts # TypeScript types
179
+ .env.example
180
+ .mcp.json # MCP server config (Linear + Claude Flow)
181
+ package.json
182
+ tsconfig.json
183
+ ```
184
+
185
+ ## Troubleshooting
186
+
187
+ ### "Missing LINEAR_API_KEY environment variable"
188
+
189
+ Make sure `.env` exists and contains your key, or export it directly:
190
+
191
+ ```bash
192
+ export LINEAR_API_KEY=lin_api_...
193
+ ```
194
+
195
+ ### "Team ID not found"
196
+
197
+ Verify your team ID:
198
+
199
+ ```bash
200
+ curl -X POST https://api.linear.app/graphql \
201
+ -H "Authorization: lin_api_YOUR_KEY" \
202
+ -H "Content-Type: application/json" \
203
+ -d '{"query": "{ viewer { teams { nodes { id name key } } } }"}'
204
+ ```
205
+
206
+ ### Claude Flow MCP not connecting
207
+
208
+ Re-register the MCP server:
209
+
210
+ ```bash
211
+ claude mcp add claude-flow -- npx -y claude-flow@alpha mcp start
212
+ ```
213
+
214
+ ### "Failed to parse PRD JSON after retry"
215
+
216
+ Claude occasionally produces unparseable output. Try running again or simplifying the feature description.
217
+
218
+ ## License
219
+
220
+ MIT
package/bin/cli.js ADDED
@@ -0,0 +1,21 @@
1
+ #!/usr/bin/env node
2
+ import { fileURLToPath } from 'url';
3
+ import { dirname, join } from 'path';
4
+ import { execFileSync } from 'child_process';
5
+
6
+ const __filename = fileURLToPath(import.meta.url);
7
+ const __dirname = dirname(__filename);
8
+
9
+ // Use tsx to run the TypeScript CLI
10
+ const tsxBin = join(__dirname, '..', 'node_modules', '.bin', 'tsx');
11
+ const cliTs = join(__dirname, 'cli.ts');
12
+
13
+ try {
14
+ execFileSync(tsxBin, [cliTs, ...process.argv.slice(2)], {
15
+ stdio: 'inherit',
16
+ env: process.env,
17
+ });
18
+ } catch (err) {
19
+ // execFileSync throws on non-zero exit — just propagate the exit code
20
+ process.exit(err.status || 1);
21
+ }
package/bin/cli.ts ADDED
@@ -0,0 +1,51 @@
1
+ #!/usr/bin/env npx tsx
2
+ const command = process.argv[2];
3
+
4
+ switch (command) {
5
+ case 'init':
6
+ import('./init.ts');
7
+ break;
8
+ case 'generate':
9
+ // Forward remaining args to generate-sdk
10
+ process.argv = [process.argv[0], process.argv[1], ...process.argv.slice(3)];
11
+ import('../src/generate-sdk.ts');
12
+ break;
13
+ case 'watch':
14
+ import('../src/bridge/index.ts').then(m => m.watch());
15
+ break;
16
+ case 'build': {
17
+ const issueId = process.argv[3];
18
+ if (!issueId) {
19
+ console.error('Usage: linear-ai-build build <issue-identifier>');
20
+ console.error('Example: linear-ai-build build WIC-42');
21
+ process.exit(1);
22
+ }
23
+ import('../src/bridge/index.ts').then(m => m.build(issueId));
24
+ break;
25
+ }
26
+ default:
27
+ console.log(`linear-ai-build - Generate user stories and build with AI agents
28
+
29
+ Commands:
30
+ init Interactive setup — credentials, MCP config, Claude Code command
31
+ generate Generate stories and push to Linear (standalone mode)
32
+ watch Watch Linear for ai-build issues and process with Claude Flow
33
+ build One-shot: process a specific Linear issue with Claude Flow
34
+
35
+ Usage:
36
+ npx linear-ai-build init
37
+ npx linear-ai-build generate "Add auth" TEAM-abc123
38
+ npx linear-ai-build watch
39
+ npx linear-ai-build build WIC-42
40
+
41
+ After running init, use the Claude Code command:
42
+ /linear:generate-stories "Add user authentication" TEAM-abc123
43
+
44
+ Environment variables for bridge:
45
+ TARGET_REPO_PATH Path to the repo where agents write code (default: cwd)
46
+ TARGET_BRANCH Base branch for new branches (default: main)
47
+ POLL_INTERVAL_MS Polling interval in ms (default: 30000)
48
+ AI_BUILD_LABEL Linear label to watch for (default: ai-build)
49
+ `);
50
+ break;
51
+ }
package/bin/init.ts ADDED
@@ -0,0 +1,252 @@
1
+ #!/usr/bin/env node
2
+ import * as fs from 'fs';
3
+ import * as path from 'path';
4
+ import * as os from 'os';
5
+ import * as readline from 'readline';
6
+ import { fileURLToPath } from 'url';
7
+
8
+ const __filename = fileURLToPath(import.meta.url);
9
+ const __dirname = path.dirname(__filename);
10
+
11
+ const COMMAND_DIR = 'linear';
12
+ const COMMAND_FILE = 'generate-stories.md';
13
+
14
+ // --- Interactive Prompts ---
15
+
16
+ function prompt(question: string, defaultValue = ''): Promise<string> {
17
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
18
+ const suffix = defaultValue ? ` [${defaultValue}]` : '';
19
+ return new Promise(resolve => {
20
+ rl.question(`${question}${suffix}: `, answer => {
21
+ rl.close();
22
+ resolve(answer.trim() || defaultValue);
23
+ });
24
+ });
25
+ }
26
+
27
+ function promptSecret(question: string): Promise<string> {
28
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
29
+ return new Promise(resolve => {
30
+ // Note: readline doesn't natively hide input, but we inform the user
31
+ rl.question(`${question}: `, answer => {
32
+ rl.close();
33
+ resolve(answer.trim());
34
+ });
35
+ });
36
+ }
37
+
38
+ // --- Credential Collection ---
39
+
40
+ async function collectCredentials(): Promise<{
41
+ linearApiKey: string;
42
+ anthropicApiKey: string;
43
+ teamId: string;
44
+ }> {
45
+ console.log('\nLinear AI Build - Setup\n');
46
+ console.log('This will configure the tool for CLI use and install the Claude Code plugin.\n');
47
+
48
+ // Check for existing .env
49
+ const envFile = path.resolve(process.cwd(), '.env');
50
+ const existing = loadExistingEnv(envFile);
51
+
52
+ // Linear API Key
53
+ console.log('1. Linear API Key');
54
+ console.log(' Get yours at: Linear Settings > Security & Access > Personal API keys');
55
+ let linearApiKey = existing.LINEAR_API_KEY || '';
56
+ if (linearApiKey) {
57
+ const masked = linearApiKey.slice(0, 8) + '...' + linearApiKey.slice(-4);
58
+ const keep = await prompt(` Found existing key (${masked}). Keep it? (y/n)`, 'y');
59
+ if (keep.toLowerCase() !== 'y') {
60
+ linearApiKey = await promptSecret(' Enter your Linear API key');
61
+ }
62
+ } else {
63
+ linearApiKey = await promptSecret(' Enter your Linear API key');
64
+ }
65
+
66
+ if (!linearApiKey) {
67
+ console.error('\n Linear API key is required. Aborting.');
68
+ process.exit(1);
69
+ }
70
+
71
+ // Anthropic API Key (optional — only needed for standalone CLI, not the Claude Code plugin)
72
+ console.log('\n2. Anthropic API Key (optional — only needed for standalone CLI)');
73
+ console.log(' Get yours at: https://console.anthropic.com/ > API Keys');
74
+ console.log(' Skip this if you only plan to use the Claude Code plugin.');
75
+ let anthropicApiKey = existing.ANTHROPIC_API_KEY || '';
76
+ if (anthropicApiKey) {
77
+ const masked = anthropicApiKey.slice(0, 8) + '...' + anthropicApiKey.slice(-4);
78
+ const keep = await prompt(` Found existing key (${masked}). Keep it? (y/n)`, 'y');
79
+ if (keep.toLowerCase() !== 'y') {
80
+ anthropicApiKey = await promptSecret(' Enter your Anthropic API key (or press Enter to skip)');
81
+ }
82
+ } else {
83
+ anthropicApiKey = await promptSecret(' Enter your Anthropic API key (or press Enter to skip)');
84
+ }
85
+
86
+ // Team ID
87
+ console.log('\n3. Default Linear Team ID');
88
+ console.log(' Find it in the URL: https://linear.app/YOUR-WORKSPACE/...');
89
+ console.log(' Or query it with: curl -X POST https://api.linear.app/graphql \\');
90
+ console.log(' -H "Authorization: <your-key>" -H "Content-Type: application/json" \\');
91
+ console.log(' -d \'{"query": "{ viewer { teams { nodes { id name } } } }"}\'');
92
+ let teamId = existing.LINEAR_TEAM_ID || '';
93
+ if (teamId) {
94
+ teamId = await prompt(` Default team ID`, teamId);
95
+ } else {
96
+ teamId = await prompt(' Default team ID (optional, can pass per-command)');
97
+ }
98
+
99
+ return { linearApiKey, anthropicApiKey, teamId };
100
+ }
101
+
102
+ function loadExistingEnv(envPath: string): Record<string, string> {
103
+ const result: Record<string, string> = {};
104
+ if (!fs.existsSync(envPath)) return result;
105
+
106
+ const content = fs.readFileSync(envPath, 'utf-8');
107
+ for (const line of content.split('\n')) {
108
+ const trimmed = line.trim();
109
+ if (!trimmed || trimmed.startsWith('#')) continue;
110
+ const eqIdx = trimmed.indexOf('=');
111
+ if (eqIdx === -1) continue;
112
+ const key = trimmed.slice(0, eqIdx).trim();
113
+ const value = trimmed.slice(eqIdx + 1).trim();
114
+ result[key] = value;
115
+ }
116
+ return result;
117
+ }
118
+
119
+ // --- .env File ---
120
+
121
+ function writeEnvFile(credentials: { linearApiKey: string; anthropicApiKey: string; teamId: string }) {
122
+ const envPath = path.resolve(process.cwd(), '.env');
123
+ const existing = loadExistingEnv(envPath);
124
+
125
+ // Merge with existing values (preserve any extra keys)
126
+ existing.LINEAR_API_KEY = credentials.linearApiKey;
127
+ if (credentials.anthropicApiKey) {
128
+ existing.ANTHROPIC_API_KEY = credentials.anthropicApiKey;
129
+ }
130
+ if (credentials.teamId) {
131
+ existing.LINEAR_TEAM_ID = credentials.teamId;
132
+ }
133
+
134
+ const lines: string[] = [];
135
+ for (const [key, value] of Object.entries(existing)) {
136
+ lines.push(`${key}=${value}`);
137
+ }
138
+
139
+ fs.writeFileSync(envPath, lines.join('\n') + '\n');
140
+ console.log(`\n Saved: ${envPath}`);
141
+ }
142
+
143
+ // --- Claude Code MCP Configuration ---
144
+
145
+ function configureLinearMcp(linearApiKey: string) {
146
+ const claudeDir = path.join(os.homedir(), '.claude');
147
+
148
+ // Write MCP config to project-level .mcp.json
149
+ // This configures the Linear MCP server so Claude Code can talk to Linear directly
150
+ const mcpConfigPath = path.resolve(process.cwd(), '.mcp.json');
151
+ let mcpConfig: Record<string, any> = {};
152
+
153
+ if (fs.existsSync(mcpConfigPath)) {
154
+ try {
155
+ mcpConfig = JSON.parse(fs.readFileSync(mcpConfigPath, 'utf-8'));
156
+ } catch {
157
+ // Start fresh if corrupted
158
+ mcpConfig = {};
159
+ }
160
+ }
161
+
162
+ if (!mcpConfig.mcpServers) {
163
+ mcpConfig.mcpServers = {};
164
+ }
165
+
166
+ mcpConfig.mcpServers.linear = {
167
+ type: 'http',
168
+ url: 'https://mcp.linear.app/mcp',
169
+ headers: {
170
+ Authorization: linearApiKey,
171
+ },
172
+ };
173
+
174
+ fs.writeFileSync(mcpConfigPath, JSON.stringify(mcpConfig, null, 2) + '\n');
175
+ console.log(` Configured Linear MCP: ${mcpConfigPath}`);
176
+ }
177
+
178
+ // --- Claude Code Command Installation ---
179
+
180
+ function installCommand() {
181
+ const targetDir = path.join(os.homedir(), '.claude', 'commands', COMMAND_DIR);
182
+ const targetFile = path.join(targetDir, COMMAND_FILE);
183
+
184
+ // Find the command template relative to this script
185
+ let sourceFile = path.resolve(__dirname, '..', 'commands', COMMAND_FILE);
186
+
187
+ if (!fs.existsSync(sourceFile)) {
188
+ // Fallback: try from cwd
189
+ sourceFile = path.resolve(process.cwd(), 'commands', COMMAND_FILE);
190
+ if (!fs.existsSync(sourceFile)) {
191
+ console.error(` Could not find command template at:\n ${path.resolve(__dirname, '..', 'commands', COMMAND_FILE)}\n ${sourceFile}`);
192
+ return false;
193
+ }
194
+ }
195
+
196
+ fs.mkdirSync(targetDir, { recursive: true });
197
+
198
+ const existed = fs.existsSync(targetFile);
199
+ fs.copyFileSync(sourceFile, targetFile);
200
+
201
+ if (existed) {
202
+ console.log(` Updated: ${targetFile}`);
203
+ } else {
204
+ console.log(` Installed: ${targetFile}`);
205
+ }
206
+
207
+ return true;
208
+ }
209
+
210
+ // --- Main ---
211
+
212
+ async function init() {
213
+ // Collect credentials interactively
214
+ const credentials = await collectCredentials();
215
+
216
+ // Write .env file
217
+ console.log('\nConfiguring environment...');
218
+ writeEnvFile(credentials);
219
+
220
+ // Configure Linear MCP for Claude Code
221
+ console.log('\nConfiguring Claude Code...');
222
+ configureLinearMcp(credentials.linearApiKey);
223
+
224
+ // Install the Claude Code command
225
+ const installed = installCommand();
226
+
227
+ // Summary
228
+ console.log('\n' + '═'.repeat(50));
229
+ console.log('Setup complete!');
230
+ console.log('═'.repeat(50));
231
+
232
+ console.log('\nCLI usage:');
233
+ if (credentials.teamId) {
234
+ console.log(` npm run generate -- "Add user authentication" ${credentials.teamId}`);
235
+ } else {
236
+ console.log(' npm run generate -- "Add user authentication" TEAM-abc123');
237
+ }
238
+
239
+ if (installed) {
240
+ console.log('\nClaude Code usage:');
241
+ if (credentials.teamId) {
242
+ console.log(` /linear:generate-stories "Add user authentication" ${credentials.teamId}`);
243
+ } else {
244
+ console.log(' /linear:generate-stories "Add user authentication" TEAM-abc123');
245
+ }
246
+ console.log('\n Linear MCP is configured — Claude can create issues directly.');
247
+ }
248
+
249
+ console.log('');
250
+ }
251
+
252
+ init();