@qelos/aidev 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.
Files changed (60) hide show
  1. package/.claude/settings.local.json +9 -0
  2. package/.cursor/rules/aidev.mdc +57 -0
  3. package/.env.aidev.example +17 -0
  4. package/CLAUDE.md +32 -0
  5. package/CONTRIBUTING.md +78 -0
  6. package/LICENSE +21 -0
  7. package/README.md +245 -0
  8. package/dist/ai/base.d.ts.map +1 -0
  9. package/dist/ai/base.js +3 -0
  10. package/dist/ai/claude.d.ts.map +1 -0
  11. package/dist/ai/claude.js +33 -0
  12. package/dist/ai/cursor.d.ts.map +1 -0
  13. package/dist/ai/cursor.js +33 -0
  14. package/dist/ai/index.d.ts.map +1 -0
  15. package/dist/ai/index.js +13 -0
  16. package/dist/cli.d.ts.map +1 -0
  17. package/dist/cli.js +74 -0
  18. package/dist/commands/help.d.ts.map +1 -0
  19. package/dist/commands/help.js +48 -0
  20. package/dist/commands/init.d.ts.map +1 -0
  21. package/dist/commands/init.js +255 -0
  22. package/dist/commands/run.d.ts.map +1 -0
  23. package/dist/commands/run.js +257 -0
  24. package/dist/commands/schedule.d.ts.map +1 -0
  25. package/dist/commands/schedule.js +191 -0
  26. package/dist/config.d.ts.map +1 -0
  27. package/dist/config.js +89 -0
  28. package/dist/git.d.ts.map +1 -0
  29. package/dist/git.js +108 -0
  30. package/dist/logger.d.ts.map +1 -0
  31. package/dist/logger.js +88 -0
  32. package/dist/platform.d.ts.map +1 -0
  33. package/dist/platform.js +69 -0
  34. package/dist/providers/base.d.ts.map +1 -0
  35. package/dist/providers/base.js +3 -0
  36. package/dist/providers/clickup.d.ts.map +1 -0
  37. package/dist/providers/clickup.js +67 -0
  38. package/dist/providers/index.d.ts.map +1 -0
  39. package/dist/providers/index.js +19 -0
  40. package/dist/types.d.ts.map +1 -0
  41. package/dist/types.js +3 -0
  42. package/package.json +28 -0
  43. package/src/ai/base.ts +11 -0
  44. package/src/ai/claude.ts +35 -0
  45. package/src/ai/cursor.ts +35 -0
  46. package/src/ai/index.ts +15 -0
  47. package/src/cli.ts +83 -0
  48. package/src/commands/help.ts +43 -0
  49. package/src/commands/init.ts +296 -0
  50. package/src/commands/run.ts +283 -0
  51. package/src/commands/schedule.ts +179 -0
  52. package/src/config.ts +59 -0
  53. package/src/git.ts +109 -0
  54. package/src/logger.ts +53 -0
  55. package/src/platform.ts +33 -0
  56. package/src/providers/base.ts +8 -0
  57. package/src/providers/clickup.ts +107 -0
  58. package/src/providers/index.ts +20 -0
  59. package/src/types.ts +33 -0
  60. package/tsconfig.json +19 -0
package/dist/types.js ADDED
@@ -0,0 +1,3 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ //# sourceMappingURL=types.js.map
package/package.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "@qelos/aidev",
3
+ "version": "0.1.0",
4
+ "description": "AI-powered task executor — polls ClickUp (and more) to implement tasks with Claude or Cursor",
5
+ "type": "commonjs",
6
+ "bin": { "aidev": "./dist/cli.js" },
7
+ "main": "./dist/cli.js",
8
+ "engines": { "node": ">=18.0.0" },
9
+ "scripts": {
10
+ "build": "tsc && node -e \"if(process.platform!=='win32')require('fs').chmodSync('dist/cli.js',0o755)\"",
11
+ "dev": "tsx src/cli.ts",
12
+ "prepublishOnly": "npm run build"
13
+ },
14
+ "keywords": ["ai", "cli", "clickup", "claude", "cursor", "automation"],
15
+ "author": "",
16
+ "license": "MIT",
17
+ "publishConfig": { "access": "public" },
18
+ "dependencies": {
19
+ "chalk": "^4.1.2",
20
+ "commander": "^12.0.0",
21
+ "dotenv": "^16.4.0"
22
+ },
23
+ "devDependencies": {
24
+ "@types/node": "^20.0.0",
25
+ "tsx": "^4.0.0",
26
+ "typescript": "^5.0.0"
27
+ }
28
+ }
package/src/ai/base.ts ADDED
@@ -0,0 +1,11 @@
1
+ export interface AIRunResult {
2
+ success: boolean;
3
+ output: string;
4
+ error: string;
5
+ }
6
+
7
+ export interface AIRunner {
8
+ name: string;
9
+ isAvailable(): boolean;
10
+ run(prompt: string, notes?: string): Promise<AIRunResult>;
11
+ }
@@ -0,0 +1,35 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { AIRunner, AIRunResult } from './base';
3
+ import { logger } from '../logger';
4
+ import { commandExists } from '../platform';
5
+
6
+ export class ClaudeRunner implements AIRunner {
7
+ readonly name = 'claude';
8
+
9
+ isAvailable(): boolean {
10
+ return commandExists('claude');
11
+ }
12
+
13
+ async run(prompt: string, notes?: string): Promise<AIRunResult> {
14
+ const fullPrompt = notes ? `${prompt}\n\nAdditional context:\n${notes}` : prompt;
15
+
16
+ logger.info('Running Claude CLI...');
17
+ logger.debug(`Prompt: ${fullPrompt.slice(0, 200)}...`);
18
+
19
+ const result = spawnSync('claude', ['-p', fullPrompt, '--dangerously-skip-permissions'], {
20
+ encoding: 'utf8',
21
+ timeout: 10 * 60 * 1000,
22
+ cwd: process.cwd(),
23
+ });
24
+
25
+ const success = result.status === 0;
26
+ const output = result.stdout || '';
27
+ const error = result.stderr || '';
28
+
29
+ if (!success) {
30
+ logger.warn(`Claude exited with status ${result.status}`);
31
+ }
32
+
33
+ return { success, output, error };
34
+ }
35
+ }
@@ -0,0 +1,35 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { AIRunner, AIRunResult } from './base';
3
+ import { logger } from '../logger';
4
+ import { commandExists } from '../platform';
5
+
6
+ export class CursorRunner implements AIRunner {
7
+ readonly name = 'cursor';
8
+
9
+ isAvailable(): boolean {
10
+ return commandExists('cursor');
11
+ }
12
+
13
+ async run(prompt: string, notes?: string): Promise<AIRunResult> {
14
+ const fullPrompt = notes ? `${prompt}\n\nAdditional context:\n${notes}` : prompt;
15
+
16
+ logger.info('Running Cursor Agent...');
17
+ logger.debug(`Prompt: ${fullPrompt.slice(0, 200)}...`);
18
+
19
+ const result = spawnSync('cursor', ['--agent', fullPrompt], {
20
+ encoding: 'utf8',
21
+ timeout: 10 * 60 * 1000,
22
+ cwd: process.cwd(),
23
+ });
24
+
25
+ const success = result.status === 0;
26
+ const output = result.stdout || '';
27
+ const error = result.stderr || '';
28
+
29
+ if (!success) {
30
+ logger.warn(`Cursor exited with status ${result.status}`);
31
+ }
32
+
33
+ return { success, output, error };
34
+ }
35
+ }
@@ -0,0 +1,15 @@
1
+ import { Config } from '../types';
2
+ import { AIRunner } from './base';
3
+ import { ClaudeRunner } from './claude';
4
+ import { CursorRunner } from './cursor';
5
+
6
+ const registry: Record<string, AIRunner> = {
7
+ claude: new ClaudeRunner(),
8
+ cursor: new CursorRunner(),
9
+ };
10
+
11
+ export function createRunners(config: Config): AIRunner[] {
12
+ return config.agents.map((name) => registry[name]);
13
+ }
14
+
15
+ export { AIRunner };
package/src/cli.ts ADDED
@@ -0,0 +1,83 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from 'commander';
3
+ import { initCommand } from './commands/init';
4
+ import { runCommand, RunFilter } from './commands/run';
5
+ import { scheduleSetCommand, scheduleGetCommand } from './commands/schedule';
6
+ import { helpCommand } from './commands/help';
7
+ import { loadConfig } from './config';
8
+ import { createProvider } from './providers';
9
+ import { createRunners } from './ai';
10
+ import { logger } from './logger';
11
+
12
+ const program = new Command();
13
+
14
+ program
15
+ .name('aidev')
16
+ .description('AI-powered task executor — implements ClickUp tasks with Claude or Cursor')
17
+ .version('0.1.0')
18
+ .option('-e, --env <path>', 'path to env file (default: .env.aidev)');
19
+
20
+ program
21
+ .command('init')
22
+ .description('Create .env.aidev from template in current directory')
23
+ .action(async () => {
24
+ await initCommand();
25
+ });
26
+
27
+ program
28
+ .command('help')
29
+ .description('Show help')
30
+ .action(() => {
31
+ helpCommand();
32
+ });
33
+
34
+ async function runWithFilter(filter: string | undefined): Promise<void> {
35
+ const validFilters: RunFilter[] = ['all', 'open', 'pending'];
36
+ const resolvedFilter: RunFilter =
37
+ filter && validFilters.includes(filter as RunFilter)
38
+ ? (filter as RunFilter)
39
+ : 'all';
40
+
41
+ if (filter && !validFilters.includes(filter as RunFilter)) {
42
+ logger.error(`Unknown filter: ${filter}. Valid options: all, open, pending`);
43
+ process.exit(1);
44
+ }
45
+
46
+ try {
47
+ const { env } = program.opts<{ env?: string }>();
48
+ const config = loadConfig(env);
49
+ const provider = createProvider(config);
50
+ const runners = createRunners(config);
51
+ await runCommand(resolvedFilter, config, provider, runners);
52
+ } catch (err) {
53
+ logger.error(String(err));
54
+ process.exit(1);
55
+ }
56
+ }
57
+
58
+ program
59
+ .command('run [filter]', { isDefault: true })
60
+ .description('Process tasks: all (default), open, or pending')
61
+ .action(async (filter?: string) => {
62
+ await runWithFilter(filter);
63
+ });
64
+
65
+ const scheduleCmd = program
66
+ .command('schedule')
67
+ .description('Manage cron schedule for aidev in current directory');
68
+
69
+ scheduleCmd
70
+ .command('set [cron]')
71
+ .description('Set cron schedule — interactive picker if no cron given')
72
+ .action(async (cron?: string) => {
73
+ await scheduleSetCommand(cron);
74
+ });
75
+
76
+ scheduleCmd
77
+ .command('get')
78
+ .description('Show current cron schedule for this directory')
79
+ .action(async () => {
80
+ await scheduleGetCommand();
81
+ });
82
+
83
+ program.parse(process.argv);
@@ -0,0 +1,43 @@
1
+ import chalk from 'chalk';
2
+
3
+ const b = chalk.bold;
4
+ const c = chalk.cyan;
5
+ const d = chalk.dim;
6
+ const g = chalk.green;
7
+
8
+ export function helpCommand(): void {
9
+ console.log(`
10
+ ${b('aidev')} ${d('v0.1.0')} — AI-powered task executor
11
+
12
+ ${b('USAGE')}
13
+ ${c('aidev')} ${d('[command]')}
14
+
15
+ ${b('COMMANDS')}
16
+ ${c('init')} Interactive setup — create ${d('.env.aidev')}
17
+ ${c('run')} Process all open + pending-with-replies tasks
18
+ ${c('run open')} Only open (non-pending) tasks
19
+ ${c('run pending')} Only pending tasks — check for human replies
20
+ ${c('schedule set')} ${d('<cron>')} Set cron schedule for this directory
21
+ ${c('schedule get')} Show current cron schedule
22
+ ${c('help')} Show this help message
23
+
24
+ ${b('EXAMPLES')}
25
+ ${d('$')} ${g('aidev init')}
26
+ ${d('$')} ${g('aidev run')}
27
+ ${d('$')} ${g('aidev run open')}
28
+ ${d('$')} ${g('aidev schedule set "*/30 * * * *"')}
29
+ ${d('$')} ${g('aidev schedule get')}
30
+
31
+ ${b('CONFIG')} ${d('.env.aidev in your project directory')}
32
+ ${d('CLICKUP_API_KEY')} ClickUp personal API token
33
+ ${d('CLICKUP_TEAM_ID')} Workspace / team ID
34
+ ${d('CLICKUP_TAG')} Tag used to filter tasks
35
+ ${d('AGENTS')} Agent order: ${c('claude,cursor')} ${d('| cursor,claude | claude | cursor')}
36
+ ${d('DEV_NOTES_MODE')} ${c('smart')} ${d('(default) | always')}
37
+ ${d('GIT_REMOTE')} Remote name ${d('(auto-detected if unset)')}
38
+ ${d('GITHUB_BASE_BRANCH')} Base branch ${d('(default: main)')}
39
+ ${d('GITHUB_REPO')} ${d('owner/repo')} for PR links
40
+
41
+ Run ${c('aidev init')} to configure interactively.
42
+ `);
43
+ }
@@ -0,0 +1,296 @@
1
+ import * as fs from 'node:fs';
2
+ import * as path from 'node:path';
3
+ import * as readline from 'node:readline/promises';
4
+ import { stdin as input, stdout as output } from 'node:process';
5
+ import { logger } from '../logger';
6
+ import { detectRemote } from '../git';
7
+ import chalk from 'chalk';
8
+
9
+ const VALID_AGENTS = ['claude', 'cursor'] as const;
10
+
11
+ // Patterns we want guaranteed in .gitignore.
12
+ // Each entry: [pattern to write, regex that matches equivalent existing lines]
13
+ const GITIGNORE_RULES: Array<[string, RegExp]> = [
14
+ ['.env.*', /^\.env[\.\*]/m],
15
+ ['*.log', /^\*\.log/m],
16
+ ];
17
+
18
+ function ensureGitignore(): void {
19
+ const gitignorePath = path.join(process.cwd(), '.gitignore');
20
+ const existing = fs.existsSync(gitignorePath)
21
+ ? fs.readFileSync(gitignorePath, 'utf8')
22
+ : '';
23
+
24
+ const missing = GITIGNORE_RULES
25
+ .filter(([, regex]) => !regex.test(existing))
26
+ .map(([pattern]) => pattern);
27
+
28
+ if (missing.length === 0) return;
29
+
30
+ const addition = (existing.endsWith('\n') || existing === '' ? '' : '\n')
31
+ + missing.join('\n') + '\n';
32
+ fs.appendFileSync(gitignorePath, addition, 'utf8');
33
+ logger.info(`.gitignore — added: ${missing.join(', ')}`);
34
+ }
35
+
36
+ interface ClickUpMember {
37
+ id: number;
38
+ username: string;
39
+ email: string;
40
+ }
41
+
42
+ interface Answers {
43
+ clickupApiKey: string;
44
+ clickupTeamId: string;
45
+ clickupTag: string;
46
+ clickupPendingStatus: string;
47
+ clickupInReviewStatus: string;
48
+ assigneeTag: string;
49
+ gitRemote: string;
50
+ githubBaseBranch: string;
51
+ githubRepo: string;
52
+ agents: string;
53
+ devNotesMode: string;
54
+ }
55
+
56
+ function dim(s: string) {
57
+ return chalk.dim(s);
58
+ }
59
+
60
+ function hint(s: string) {
61
+ return chalk.dim(`(${s})`);
62
+ }
63
+
64
+ async function ask(
65
+ rl: readline.Interface,
66
+ question: string,
67
+ defaultVal = '',
68
+ required = false
69
+ ): Promise<string> {
70
+ const suffix = defaultVal ? chalk.dim(` [${defaultVal}]`) : '';
71
+ while (true) {
72
+ const raw = await rl.question(` ${question}${suffix}: `);
73
+ const val = raw.trim() || defaultVal;
74
+ if (required && !val) {
75
+ console.log(chalk.yellow(` This field is required.`));
76
+ continue;
77
+ }
78
+ return val;
79
+ }
80
+ }
81
+
82
+ async function choose(
83
+ rl: readline.Interface,
84
+ question: string,
85
+ options: string[],
86
+ defaultVal: string
87
+ ): Promise<string> {
88
+ const opts = options
89
+ .map((o) => (o === defaultVal ? chalk.cyan(o) : o))
90
+ .join(chalk.dim(' | '));
91
+ while (true) {
92
+ const raw = await rl.question(` ${question} ${dim(`[${opts}]`)}: `);
93
+ const val = raw.trim() || defaultVal;
94
+ if (!options.includes(val)) {
95
+ console.log(chalk.yellow(` Choose one of: ${options.join(', ')}`));
96
+ continue;
97
+ }
98
+ return val;
99
+ }
100
+ }
101
+
102
+ async function pickAgents(rl: readline.Interface): Promise<string> {
103
+ const available = [...VALID_AGENTS];
104
+
105
+ console.log(`\n Available agents:`);
106
+ available.forEach((a, i) => console.log(` ${chalk.cyan(String(i + 1))}. ${a}`));
107
+
108
+ console.log(
109
+ `\n Enter agents ${hint('numbers or names, comma-separated — first = primary, rest = fallback')}`
110
+ );
111
+
112
+ while (true) {
113
+ const raw = await rl.question(` Agents in order ${dim(`[${available.join(',')}]`)}: `);
114
+
115
+ if (!raw.trim()) return available.join(',');
116
+
117
+ const parts = raw.split(',').map((s) => s.trim().toLowerCase()).filter(Boolean);
118
+ const resolved = parts.map((p) => {
119
+ const idx = parseInt(p, 10);
120
+ if (!isNaN(idx) && idx >= 1 && idx <= available.length) return available[idx - 1];
121
+ return p;
122
+ });
123
+
124
+ const invalid = resolved.filter((r) => !available.includes(r as typeof available[number]));
125
+ if (invalid.length) {
126
+ console.log(chalk.yellow(` Unknown agent(s): ${invalid.join(', ')}. Valid: ${available.join(', ')}`));
127
+ continue;
128
+ }
129
+
130
+ const unique = [...new Set(resolved)];
131
+ if (unique.length !== resolved.length) {
132
+ console.log(chalk.yellow(` Duplicate agents removed: ${unique.join(', ')}`));
133
+ }
134
+ return unique.join(',');
135
+ }
136
+ }
137
+
138
+ async function fetchCurrentUser(apiKey: string): Promise<ClickUpMember | null> {
139
+ try {
140
+ const res = await fetch('https://api.clickup.com/api/v2/user', {
141
+ headers: { Authorization: apiKey },
142
+ });
143
+ if (!res.ok) return null;
144
+ const data = await res.json() as { user: ClickUpMember };
145
+ return data.user;
146
+ } catch {
147
+ return null;
148
+ }
149
+ }
150
+
151
+ async function pickAssignee(rl: readline.Interface, apiKey: string): Promise<string> {
152
+ process.stdout.write(` ${chalk.dim('Fetching current user...')}\r`);
153
+ const user = await fetchCurrentUser(apiKey);
154
+ process.stdout.write(' \r');
155
+
156
+ if (!user) {
157
+ return ask(rl, `Assignee tag ${hint('optional — could not fetch user')}`, '');
158
+ }
159
+
160
+ const display = user.username && user.username !== 'null'
161
+ ? `${user.username} <${user.email}>`
162
+ : user.email;
163
+ return ask(rl, `Assignee tag`, display);
164
+ }
165
+
166
+ function section(title: string) {
167
+ console.log('\n' + chalk.bold.underline(title));
168
+ }
169
+
170
+ /** Wraps value in double quotes if it contains spaces or special chars. */
171
+ function envVal(val: string): string {
172
+ return /[\s#"']/.test(val) ? `"${val.replace(/"/g, '\\"')}"` : val;
173
+ }
174
+
175
+ function line(key: string, val: string): string | null {
176
+ return val ? `${key}=${envVal(val)}` : null;
177
+ }
178
+
179
+ function renderEnv(a: Answers): string {
180
+ const lines = [
181
+ `PROVIDER=clickup`,
182
+ line('CLICKUP_API_KEY', a.clickupApiKey),
183
+ line('CLICKUP_TEAM_ID', a.clickupTeamId),
184
+ line('CLICKUP_TAG', a.clickupTag),
185
+ `CLICKUP_PENDING_STATUS=${envVal(a.clickupPendingStatus)}`,
186
+ `CLICKUP_IN_REVIEW_STATUS=${envVal(a.clickupInReviewStatus)}`,
187
+ ``,
188
+ line('ASSIGNEE_TAG', a.assigneeTag),
189
+ `GIT_REMOTE=${envVal(a.gitRemote)}`,
190
+ `GITHUB_BASE_BRANCH=${envVal(a.githubBaseBranch)}`,
191
+ line('GITHUB_REPO', a.githubRepo),
192
+ ``,
193
+ `# Agents to use, in fallback order (comma-separated: claude, cursor)`,
194
+ `AGENTS=${a.agents}`,
195
+ ``,
196
+ `# DEV_NOTES_MODE: smart (only ask when unclear) | always (ask before every task)`,
197
+ `DEV_NOTES_MODE=${a.devNotesMode}`,
198
+ ``,
199
+ ];
200
+ return lines.filter((l) => l !== null).join('\n');
201
+ }
202
+
203
+ export async function initCommand(): Promise<void> {
204
+ const dest = path.join(process.cwd(), '.env.aidev');
205
+
206
+ if (fs.existsSync(dest)) {
207
+ const rl0 = readline.createInterface({ input, output });
208
+ const overwrite = await rl0.question(
209
+ chalk.yellow('.env.aidev already exists. Reconfigure? ') + dim('[y/N] ')
210
+ );
211
+ rl0.close();
212
+ if (overwrite.trim().toLowerCase() !== 'y') {
213
+ logger.info('Keeping existing .env.aidev.');
214
+ return;
215
+ }
216
+ console.log();
217
+ }
218
+
219
+ console.log(chalk.bold('\naidev setup') + dim(' — press Enter to accept defaults\n'));
220
+
221
+ const rl = readline.createInterface({ input, output });
222
+
223
+ try {
224
+ // ── ClickUp ──────────────────────────────────────────────
225
+ section('ClickUp');
226
+ const globalEnvHint = hint('leave blank to use global env var');
227
+ const clickupApiKey = await ask(rl, `API key ${globalEnvHint}`, '');
228
+ const clickupTeamId = await ask(rl, `Team / workspace ID ${globalEnvHint}`, '');
229
+ const folderName = path.basename(process.cwd());
230
+ const clickupTag = await ask(
231
+ rl,
232
+ `Tag to filter tasks ${hint('tasks with this tag will be picked up')}`,
233
+ folderName
234
+ );
235
+ const clickupPendingStatus = await ask(rl, 'Pending status name', 'pending');
236
+ const clickupInReviewStatus = await ask(rl, 'In-review status name', 'review');
237
+
238
+ // ── Git / GitHub ─────────────────────────────────────────
239
+ section('Git & GitHub');
240
+ const detectedRemote = detectRemote() ?? 'origin';
241
+ const gitRemote = await ask(rl, 'Git remote', detectedRemote);
242
+ const githubBaseBranch = await ask(rl, 'Base branch', 'main');
243
+ const githubRepo = await ask(
244
+ rl,
245
+ `GitHub repo ${hint('owner/repo — used for PR links, optional')}`,
246
+ ''
247
+ );
248
+
249
+ // ── AI agents ────────────────────────────────────────────
250
+ section('AI agents');
251
+ const agents = await pickAgents(rl);
252
+ const devNotesMode = await choose(
253
+ rl,
254
+ `Dev notes mode ${hint('smart = ask AI if unclear, always = ask before every task')}`,
255
+ ['smart', 'always'],
256
+ 'smart'
257
+ );
258
+
259
+ // ── Assignee ─────────────────────────────────────────────
260
+ section('Assignee');
261
+ const effectiveApiKey = clickupApiKey || process.env.CLICKUP_API_KEY || '';
262
+ let assigneeTag: string;
263
+
264
+ if (effectiveApiKey) {
265
+ assigneeTag = await pickAssignee(rl, effectiveApiKey);
266
+ } else {
267
+ assigneeTag = await ask(
268
+ rl,
269
+ `Assignee tag ${hint('optional — provide API key above to auto-detect')}`,
270
+ ''
271
+ );
272
+ }
273
+
274
+ const answers: Answers = {
275
+ clickupApiKey,
276
+ clickupTeamId,
277
+ clickupTag,
278
+ clickupPendingStatus,
279
+ clickupInReviewStatus,
280
+ assigneeTag,
281
+ gitRemote,
282
+ githubBaseBranch,
283
+ githubRepo,
284
+ agents,
285
+ devNotesMode,
286
+ };
287
+
288
+ ensureGitignore();
289
+ fs.writeFileSync(dest, renderEnv(answers), 'utf8');
290
+ console.log();
291
+ logger.success(`.env.aidev written to ${dest}`);
292
+ logger.info(`Agents: ${agents} ${dim('(first = primary, rest = fallback)')}`);
293
+ } finally {
294
+ rl.close();
295
+ }
296
+ }