twin-cli 0.1.0 → 0.2.1
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 +103 -51
- package/package.json +2 -2
- package/src/build.js +67 -29
- package/src/generate.js +3 -3
- package/src/init.js +2 -9
- package/src/llm.js +42 -49
- package/src/plan.js +15 -8
package/README.md
CHANGED
|
@@ -1,85 +1,139 @@
|
|
|
1
1
|
# Twin-Driven Development
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Old TDD meant write the tests first. New TDD means write the twin first.
|
|
4
4
|
|
|
5
|
-
A `.twin` file encodes
|
|
6
|
-
|
|
7
|
-
The twin is not a PRD. It's not a project spec. It's **you**.
|
|
5
|
+
A `.twin` file encodes how you think. Drop it into any project. Your AI does not wait for instructions. It decides what to build next and builds it. Not human in the loop. Twin in the loop.
|
|
8
6
|
|
|
9
7
|
## The Problem
|
|
10
8
|
|
|
11
|
-
|
|
9
|
+
You give an agent a task list. It finishes. It stops. Now you have to decide what comes next.
|
|
12
10
|
|
|
13
|
-
|
|
11
|
+
So you check at 2am. Write the next batch. Go back to sleep. Wake at 5am. Check again. You are not writing code but you are on call around the clock.
|
|
14
12
|
|
|
15
|
-
|
|
13
|
+
This is Vampire Coding. Hands-off in theory. Consumed in practice.
|
|
16
14
|
|
|
17
|
-
|
|
15
|
+
The root cause: agents do not know what you would build next. They can execute. They cannot decide.
|
|
18
16
|
|
|
19
|
-
|
|
17
|
+
## The Fix
|
|
20
18
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
twin
|
|
19
|
+
Three commands.
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
twin init # encode how you think
|
|
23
|
+
twin plan # your twin decides what to build next
|
|
24
|
+
twin build # your twin builds it without you
|
|
25
25
|
```
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
The difference between a `.twin` file and a Claude MD or a rules file: those are instructions. This is a decision-maker. Your twin knows how you think and decides what comes next.
|
|
28
|
+
|
|
29
|
+
Run `twin plan` once and you get the next batch of features you would have chosen. Run it again and you get the batch after that. Keep running it and you are watching your own product roadmap unfold without writing a single task yourself.
|
|
30
|
+
|
|
31
|
+
## Before You Start
|
|
28
32
|
|
|
29
33
|
- **Node.js 18+** — [nodejs.org](https://nodejs.org)
|
|
30
|
-
- **
|
|
31
|
-
- **Claude Code** — [docs.anthropic.com/en/docs/claude-code](https://docs.anthropic.com/en/docs/claude-code) (for `twin build`)
|
|
34
|
+
- **Claude Code** — [docs.anthropic.com](https://docs.anthropic.com/en/docs/claude-code) (powers all three commands)
|
|
32
35
|
|
|
33
36
|
## Quick Start
|
|
34
37
|
|
|
38
|
+
Each twin project starts in its own folder. Create one, then run from inside it.
|
|
39
|
+
|
|
35
40
|
```bash
|
|
36
|
-
#
|
|
37
|
-
|
|
41
|
+
# 0. Create a project folder
|
|
42
|
+
mkdir my-app && cd my-app
|
|
38
43
|
|
|
39
|
-
#
|
|
44
|
+
# 1. Create your twin (once)
|
|
40
45
|
npx twin-cli init
|
|
41
46
|
# → Asks your name, then 5 questions about how you build
|
|
42
|
-
# → Generates
|
|
47
|
+
# → Generates yourname.twin
|
|
43
48
|
|
|
44
|
-
#
|
|
49
|
+
# 2. Generate your first plan
|
|
45
50
|
npx twin-cli plan
|
|
46
51
|
# → Reads your twin, asks about your product, writes prd.json
|
|
47
52
|
|
|
48
|
-
#
|
|
53
|
+
# 3. Let your twin build
|
|
54
|
+
npx twin-cli build
|
|
55
|
+
# → Spawns Claude Code in a loop
|
|
56
|
+
# → Builds each story, updates prd.json as it goes
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Example: Building a Habit Tracker
|
|
60
|
+
|
|
61
|
+
A full walkthrough from zero to working app.
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
# Create the project
|
|
65
|
+
mkdir habit-tracker && cd habit-tracker
|
|
66
|
+
|
|
67
|
+
# Create your twin
|
|
68
|
+
npx twin-cli init
|
|
69
|
+
# → "What should we call you?" → Dru
|
|
70
|
+
# → Answer 5 questions about how you build
|
|
71
|
+
# → Generates dru.twin
|
|
72
|
+
|
|
73
|
+
# Generate the plan
|
|
74
|
+
npx twin-cli plan
|
|
75
|
+
# → "What are you building?" → A habit app with GitHub-style tracking and timers
|
|
76
|
+
# → "Who is it for?" → People who want to build daily habits
|
|
77
|
+
# → Writes prd.json with 3-5 user stories based on YOUR taste
|
|
78
|
+
|
|
79
|
+
# Build it
|
|
80
|
+
npx twin-cli build
|
|
81
|
+
# → Claude picks the first story, builds it, marks it done
|
|
82
|
+
# → Picks the next story, builds it, marks it done
|
|
83
|
+
# → You watch it happen in real time
|
|
84
|
+
|
|
85
|
+
# Want more features? Plan again.
|
|
86
|
+
npx twin-cli plan
|
|
87
|
+
# → Sees what is done, generates the next batch
|
|
49
88
|
npx twin-cli build
|
|
50
|
-
# → Spawns Claude Code in a loop, builds each story, updates prd.json as it goes
|
|
51
89
|
```
|
|
52
90
|
|
|
53
|
-
|
|
91
|
+
Your `.twin` file is portable. Copy it into any new project and run `twin plan` to start.
|
|
92
|
+
|
|
93
|
+
## Project Ideas
|
|
94
|
+
|
|
95
|
+
Twin works best on new projects. Some things to try:
|
|
54
96
|
|
|
55
|
-
|
|
97
|
+
- Habit tracker with streaks and daily timers
|
|
98
|
+
- Personal landing page with email capture and a changelog
|
|
99
|
+
- Micro-SaaS dashboard for tracking one metric
|
|
100
|
+
- CLI tool that solves a problem you keep solving by hand
|
|
101
|
+
- PWA that replaces a spreadsheet you use each day
|
|
56
102
|
|
|
57
|
-
|
|
103
|
+
## The Loop
|
|
58
104
|
|
|
59
|
-
|
|
60
|
-
2. **`twin plan`** — your twin generates tasks that match how you'd prioritize
|
|
61
|
-
3. **`twin build`** — your twin builds autonomously, updating prd.json as stories complete
|
|
62
|
-
4. **`twin plan`** again — your twin sees what's done and plans what's next
|
|
105
|
+
`init → plan → build → plan`
|
|
63
106
|
|
|
64
|
-
|
|
107
|
+
Your twin drives the whole cycle.
|
|
108
|
+
|
|
109
|
+
1. **`twin init`** — encode how you think (once)
|
|
110
|
+
2. **`twin plan`** — your twin generates tasks that match how you prioritize
|
|
111
|
+
3. **`twin build`** — your twin builds on its own, updating `prd.json` as stories complete
|
|
112
|
+
4. **`twin plan` again** — your twin reads what shipped and decides what comes next
|
|
113
|
+
|
|
114
|
+
You did not write a task list. The twin wrote it. You did not pick the next feature. The twin picked it. That is the shift: from reactive to proactive.
|
|
115
|
+
|
|
116
|
+
Each iteration, Claude Code starts fresh but reads the files on disk. Your twin. The PRD. A progress log. The files are the memory. Your taste stays consistent across runs.
|
|
65
117
|
|
|
66
118
|
## Commands
|
|
67
119
|
|
|
68
120
|
### `twin init`
|
|
69
|
-
|
|
121
|
+
|
|
122
|
+
Asks your name, then 5 questions about how you build things. Generates `yourname.twin`.
|
|
70
123
|
|
|
71
124
|
### `twin plan`
|
|
72
|
-
Reads your `.twin` file + project context, generates 3-5 atomic capabilities that match your taste.
|
|
73
125
|
|
|
74
|
-
Writes
|
|
126
|
+
Reads your `.twin` file and project context. Generates 3-5 capabilities that match your taste. Writes `prd.json` with user stories and status tracking.
|
|
75
127
|
|
|
76
|
-
If no `product.md` exists, `twin plan` asks 2 quick questions to set up your project context first. Running it again adds new stories without duplicating
|
|
128
|
+
If no `product.md` exists, `twin plan` asks 2 quick questions to set up your project context first. Running it again adds new stories without duplicating old ones.
|
|
77
129
|
|
|
78
130
|
### `twin build`
|
|
131
|
+
|
|
79
132
|
Runs an autonomous build loop using Claude Code. Each iteration:
|
|
80
|
-
|
|
133
|
+
|
|
134
|
+
- Reads your twin file for how you think
|
|
81
135
|
- Reads `prd.json` for open stories
|
|
82
|
-
- Picks the next story to build (
|
|
136
|
+
- Picks the next story to build (guided by your taste)
|
|
83
137
|
- Builds it, commits, and marks it done in `prd.json`
|
|
84
138
|
- Appends learnings to `progress.md`
|
|
85
139
|
- Repeats until all stories are done or max iterations hit
|
|
@@ -89,29 +143,27 @@ twin build # default: 5 iterations
|
|
|
89
143
|
twin build --max-iterations 10 # custom limit
|
|
90
144
|
```
|
|
91
145
|
|
|
92
|
-
Requires
|
|
146
|
+
Requires Claude Code installed and available in your PATH.
|
|
93
147
|
|
|
94
148
|
## What Goes in a `.twin` File
|
|
95
149
|
|
|
96
|
-
- **Execution
|
|
97
|
-
- **Quality
|
|
98
|
-
- **Decision-
|
|
99
|
-
- **
|
|
100
|
-
- **Anti-
|
|
150
|
+
- **Execution bias** — do you plan first or build first? Ship rough or wait for polish?
|
|
151
|
+
- **Quality compass** — how do you define "done"? What does "good" look like to you?
|
|
152
|
+
- **Decision-making style** — how do you get unstuck? How do you weigh trade-offs?
|
|
153
|
+
- **Strong beliefs** — what do you believe that most would disagree with?
|
|
154
|
+
- **Anti-patterns** — what do you refuse to do?
|
|
101
155
|
|
|
102
156
|
## What Does NOT Go in a `.twin` File
|
|
103
157
|
|
|
104
|
-
- Your product's target user (that
|
|
105
|
-
- Your tech stack (that
|
|
106
|
-
- Your roadmap (that
|
|
107
|
-
|
|
108
|
-
**The twin encodes how you think. Project files encode what you're building. The twin is the founder. The project files are the company.**
|
|
158
|
+
- Your product's target user (that goes in a product doc)
|
|
159
|
+
- Your tech stack (that goes in project config)
|
|
160
|
+
- Your roadmap (that goes in a PRD)
|
|
109
161
|
|
|
110
|
-
|
|
162
|
+
The twin encodes how you think. The project files encode what you are building. The twin is the founder. The project files are the company.
|
|
111
163
|
|
|
112
|
-
|
|
164
|
+
## What Comes Next
|
|
113
165
|
|
|
114
|
-
|
|
166
|
+
`twin tweak` for natural language updates. `twin share` for publishing your taste publicly.
|
|
115
167
|
|
|
116
168
|
## License
|
|
117
169
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "twin-cli",
|
|
3
|
-
"version": "0.1
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.2.1",
|
|
4
|
+
"description": "Your twin builds while you sleep. Not human in the loop. Twin in the loop.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"twin": "./bin/twin.js"
|
package/src/build.js
CHANGED
|
@@ -56,27 +56,33 @@ ${progressContent ? `\n## progress.md\n${progressContent}` : ''}
|
|
|
56
56
|
`;
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
function
|
|
59
|
+
function parseEvent(jsonLine) {
|
|
60
60
|
try {
|
|
61
61
|
const event = JSON.parse(jsonLine);
|
|
62
62
|
|
|
63
63
|
// Text delta from assistant message streaming
|
|
64
64
|
if (event.type === 'assistant' && event.message?.content) {
|
|
65
|
-
|
|
66
|
-
return event.message.content
|
|
65
|
+
const text = event.message.content
|
|
67
66
|
.filter((block) => block.type === 'text')
|
|
68
67
|
.map((block) => block.text)
|
|
69
68
|
.join('');
|
|
69
|
+
if (text) return { type: 'text', text };
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
// Partial streaming text delta
|
|
73
73
|
if (event.type === 'content_block_delta' && event.delta?.type === 'text_delta') {
|
|
74
|
-
return event.delta.text;
|
|
74
|
+
return { type: 'text', text: event.delta.text };
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
-
//
|
|
78
|
-
if (event.type === '
|
|
79
|
-
|
|
77
|
+
// Tool use — show what Claude is doing
|
|
78
|
+
if (event.type === 'content_block_start' && event.content_block?.type === 'tool_use') {
|
|
79
|
+
const name = event.content_block.name || 'working';
|
|
80
|
+
return { type: 'tool', name };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Tool result
|
|
84
|
+
if (event.type === 'content_block_start' && event.content_block?.type === 'tool_result') {
|
|
85
|
+
return { type: 'tool_done' };
|
|
80
86
|
}
|
|
81
87
|
} catch {
|
|
82
88
|
// Not valid JSON or unexpected shape — skip
|
|
@@ -98,16 +104,41 @@ function runIteration(prompt, cwd) {
|
|
|
98
104
|
|
|
99
105
|
let output = '';
|
|
100
106
|
let buffer = '';
|
|
101
|
-
let
|
|
107
|
+
let lastActivity = Date.now();
|
|
108
|
+
let statusLine = false; // true when a status line is showing
|
|
109
|
+
|
|
110
|
+
const TOOL_LABELS = {
|
|
111
|
+
Read: 'Reading file',
|
|
112
|
+
Write: 'Writing file',
|
|
113
|
+
Edit: 'Editing file',
|
|
114
|
+
Bash: 'Running command',
|
|
115
|
+
Glob: 'Searching files',
|
|
116
|
+
Grep: 'Searching code',
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
function clearStatus() {
|
|
120
|
+
if (statusLine) {
|
|
121
|
+
process.stdout.write('\r\x1b[K');
|
|
122
|
+
statusLine = false;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function showStatus(msg) {
|
|
127
|
+
clearStatus();
|
|
128
|
+
const elapsed = Math.round((Date.now() - startTime) / 1000);
|
|
129
|
+
process.stdout.write(`\r\x1b[2m${msg} (${elapsed}s)\x1b[0m`);
|
|
130
|
+
statusLine = true;
|
|
131
|
+
}
|
|
102
132
|
|
|
103
133
|
// Heartbeat timer — show elapsed time while waiting
|
|
104
134
|
const startTime = Date.now();
|
|
105
135
|
const heartbeat = setInterval(() => {
|
|
106
136
|
const elapsed = Math.round((Date.now() - startTime) / 1000);
|
|
107
|
-
|
|
108
|
-
|
|
137
|
+
const idle = Date.now() - lastActivity;
|
|
138
|
+
if (idle > 5_000) {
|
|
139
|
+
showStatus('Working...');
|
|
109
140
|
}
|
|
110
|
-
},
|
|
141
|
+
}, 5_000);
|
|
111
142
|
|
|
112
143
|
claude.stdout.on('data', (chunk) => {
|
|
113
144
|
buffer += chunk.toString();
|
|
@@ -116,15 +147,20 @@ function runIteration(prompt, cwd) {
|
|
|
116
147
|
|
|
117
148
|
for (const line of lines) {
|
|
118
149
|
if (!line.trim()) continue;
|
|
119
|
-
const
|
|
120
|
-
if (
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
process.stdout.write(text);
|
|
127
|
-
output += text;
|
|
150
|
+
const event = parseEvent(line);
|
|
151
|
+
if (!event) continue;
|
|
152
|
+
|
|
153
|
+
lastActivity = Date.now();
|
|
154
|
+
|
|
155
|
+
if (event.type === 'text') {
|
|
156
|
+
clearStatus();
|
|
157
|
+
process.stdout.write(event.text);
|
|
158
|
+
output += event.text;
|
|
159
|
+
} else if (event.type === 'tool') {
|
|
160
|
+
const label = TOOL_LABELS[event.name] || event.name;
|
|
161
|
+
showStatus(label);
|
|
162
|
+
} else if (event.type === 'tool_done') {
|
|
163
|
+
clearStatus();
|
|
128
164
|
}
|
|
129
165
|
}
|
|
130
166
|
});
|
|
@@ -135,17 +171,15 @@ function runIteration(prompt, cwd) {
|
|
|
135
171
|
|
|
136
172
|
claude.on('close', (code) => {
|
|
137
173
|
clearInterval(heartbeat);
|
|
174
|
+
clearStatus();
|
|
138
175
|
// Process any remaining buffer
|
|
139
176
|
if (buffer.trim()) {
|
|
140
|
-
const
|
|
141
|
-
if (text) {
|
|
142
|
-
process.stdout.write(text);
|
|
143
|
-
output += text;
|
|
177
|
+
const event = parseEvent(buffer.trim());
|
|
178
|
+
if (event?.type === 'text') {
|
|
179
|
+
process.stdout.write(event.text);
|
|
180
|
+
output += event.text;
|
|
144
181
|
}
|
|
145
182
|
}
|
|
146
|
-
if (!hasReceivedText) {
|
|
147
|
-
process.stdout.write('\r\x1b[K'); // Clear heartbeat line
|
|
148
|
-
}
|
|
149
183
|
resolvePromise({ output, code });
|
|
150
184
|
});
|
|
151
185
|
|
|
@@ -214,7 +248,9 @@ export async function build(maxIterations = 5) {
|
|
|
214
248
|
if (output.includes(ALL_DONE_SIGNAL)) {
|
|
215
249
|
console.log(`\n${'='.repeat(60)}`);
|
|
216
250
|
console.log(' All stories complete!');
|
|
217
|
-
console.log(`${'='.repeat(60)}
|
|
251
|
+
console.log(`${'='.repeat(60)}`);
|
|
252
|
+
console.log('\nNext step — plan more features:');
|
|
253
|
+
console.log(' twin plan\n');
|
|
218
254
|
break;
|
|
219
255
|
}
|
|
220
256
|
|
|
@@ -225,7 +261,9 @@ export async function build(maxIterations = 5) {
|
|
|
225
261
|
if (remaining.length === 0) {
|
|
226
262
|
console.log(`\n${'='.repeat(60)}`);
|
|
227
263
|
console.log(' All stories complete!');
|
|
228
|
-
console.log(`${'='.repeat(60)}
|
|
264
|
+
console.log(`${'='.repeat(60)}`);
|
|
265
|
+
console.log('\nNext step — plan more features:');
|
|
266
|
+
console.log(' twin plan\n');
|
|
229
267
|
break;
|
|
230
268
|
}
|
|
231
269
|
console.log(`\nStory complete. ${remaining.length} remaining.\n`);
|
package/src/generate.js
CHANGED
|
@@ -42,9 +42,8 @@ Rules:
|
|
|
42
42
|
- Keep it under 80 lines total
|
|
43
43
|
- No preamble, no explanation — just the .twin file content`;
|
|
44
44
|
|
|
45
|
-
export async function generateTwin(
|
|
45
|
+
export async function generateTwin(name, interviewText) {
|
|
46
46
|
const content = await callLLM(
|
|
47
|
-
apiKey,
|
|
48
47
|
SYSTEM_PROMPT,
|
|
49
48
|
`The builder's name is ${name}.\n\nHere are the interview answers. Generate the .twin file.\n\n${interviewText}`,
|
|
50
49
|
);
|
|
@@ -53,5 +52,6 @@ export async function generateTwin(apiKey, name, interviewText) {
|
|
|
53
52
|
const outPath = resolve(process.cwd(), filename);
|
|
54
53
|
await writeFile(outPath, content, 'utf-8');
|
|
55
54
|
console.log(`Done! Your twin file is at: ${outPath}`);
|
|
56
|
-
console.log('\
|
|
55
|
+
console.log('\nNext step — plan what to build:');
|
|
56
|
+
console.log(' twin plan');
|
|
57
57
|
}
|
package/src/init.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { requireKey, checkKey } from './llm.js';
|
|
2
1
|
import { createPrompter } from './prompt.js';
|
|
3
2
|
import { generateTwin } from './generate.js';
|
|
4
3
|
|
|
@@ -11,13 +10,7 @@ const QUESTIONS = [
|
|
|
11
10
|
];
|
|
12
11
|
|
|
13
12
|
export async function init() {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
console.log('\nChecking API connection...');
|
|
17
|
-
await checkKey(key);
|
|
18
|
-
console.log('Connected.\n');
|
|
19
|
-
|
|
20
|
-
console.log('--- twin init ---');
|
|
13
|
+
console.log('\n--- twin init ---');
|
|
21
14
|
console.log('Answer a few questions. Say as much or as little as you want.');
|
|
22
15
|
console.log('Your answers will be used to generate your .twin file.\n');
|
|
23
16
|
|
|
@@ -35,5 +28,5 @@ export async function init() {
|
|
|
35
28
|
console.log('\nGenerating your .twin file...\n');
|
|
36
29
|
|
|
37
30
|
const paired = answers.map((a) => `Q: ${a.question}\nA: ${a.answer}`).join('\n\n');
|
|
38
|
-
await generateTwin(
|
|
31
|
+
await generateTwin(name, paired);
|
|
39
32
|
}
|
package/src/llm.js
CHANGED
|
@@ -1,59 +1,52 @@
|
|
|
1
|
-
|
|
2
|
-
const key = process.env.OPENROUTER_API_KEY;
|
|
3
|
-
if (!key) {
|
|
4
|
-
console.log('\nSet your OpenRouter API key first:');
|
|
5
|
-
console.log(' export OPENROUTER_API_KEY="your-key-here"');
|
|
6
|
-
console.log('\nGet one at https://openrouter.ai/keys\n');
|
|
7
|
-
process.exit(1);
|
|
8
|
-
}
|
|
9
|
-
return key;
|
|
10
|
-
}
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
11
2
|
|
|
12
|
-
export async function
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
3
|
+
export async function callLLM(systemPrompt, userMessage) {
|
|
4
|
+
const prompt = `${systemPrompt}\n\n${userMessage}`;
|
|
5
|
+
|
|
6
|
+
const output = await new Promise((resolve, reject) => {
|
|
7
|
+
const claude = spawn('claude', ['--print'], {
|
|
8
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
9
|
+
env: { ...process.env },
|
|
16
10
|
});
|
|
17
|
-
if (!res.ok) {
|
|
18
|
-
console.error(`\nAPI key check failed (${res.status}). Make sure your OPENROUTER_API_KEY is valid.`);
|
|
19
|
-
console.log('Get one at https://openrouter.ai/keys\n');
|
|
20
|
-
process.exit(1);
|
|
21
|
-
}
|
|
22
|
-
} catch (e) {
|
|
23
|
-
console.error('\nCould not connect to OpenRouter. Check your internet connection.\n');
|
|
24
|
-
process.exit(1);
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
11
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
method: 'POST',
|
|
31
|
-
headers: {
|
|
32
|
-
'Authorization': `Bearer ${apiKey}`,
|
|
33
|
-
'Content-Type': 'application/json',
|
|
34
|
-
},
|
|
35
|
-
body: JSON.stringify({
|
|
36
|
-
model: 'google/gemini-3-flash-preview',
|
|
37
|
-
messages: [
|
|
38
|
-
{ role: 'system', content: systemPrompt },
|
|
39
|
-
{ role: 'user', content: userMessage },
|
|
40
|
-
],
|
|
41
|
-
}),
|
|
42
|
-
});
|
|
12
|
+
let stdout = '';
|
|
13
|
+
let stderr = '';
|
|
43
14
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
15
|
+
claude.stdout.on('data', (chunk) => {
|
|
16
|
+
stdout += chunk.toString();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
claude.stderr.on('data', (chunk) => {
|
|
20
|
+
stderr += chunk.toString();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
claude.on('close', (code) => {
|
|
24
|
+
if (code !== 0) {
|
|
25
|
+
console.error(`Claude Code exited with code ${code}.`);
|
|
26
|
+
if (stderr) console.error(stderr);
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
resolve(stdout.trim());
|
|
30
|
+
});
|
|
49
31
|
|
|
50
|
-
|
|
51
|
-
|
|
32
|
+
claude.on('error', (err) => {
|
|
33
|
+
if (err.code === 'ENOENT') {
|
|
34
|
+
console.error('\nClaude Code is not installed or not in PATH.');
|
|
35
|
+
console.error('Install it: https://docs.anthropic.com/en/docs/claude-code\n');
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
console.error(`\nFailed to spawn Claude: ${err.message}\n`);
|
|
39
|
+
process.exit(1);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
claude.stdin.write(prompt);
|
|
43
|
+
claude.stdin.end();
|
|
44
|
+
});
|
|
52
45
|
|
|
53
|
-
if (!
|
|
54
|
-
console.error('No content returned from
|
|
46
|
+
if (!output) {
|
|
47
|
+
console.error('No content returned from Claude Code.');
|
|
55
48
|
process.exit(1);
|
|
56
49
|
}
|
|
57
50
|
|
|
58
|
-
return
|
|
51
|
+
return output;
|
|
59
52
|
}
|
package/src/plan.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { readFile, writeFile, readdir } from 'node:fs/promises';
|
|
2
2
|
import { resolve, basename } from 'node:path';
|
|
3
|
-
import {
|
|
3
|
+
import { callLLM } from './llm.js';
|
|
4
4
|
import { createPrompter } from './prompt.js';
|
|
5
5
|
|
|
6
6
|
const TASK_SYSTEM_PROMPT = `You are a taste-aware product planner. You receive:
|
|
@@ -72,12 +72,6 @@ function parseLLMJson(raw) {
|
|
|
72
72
|
}
|
|
73
73
|
|
|
74
74
|
export async function plan() {
|
|
75
|
-
const key = requireKey();
|
|
76
|
-
|
|
77
|
-
console.log('\nChecking API connection...');
|
|
78
|
-
await checkKey(key);
|
|
79
|
-
console.log('Connected.\n');
|
|
80
|
-
|
|
81
75
|
// Find *.twin file — required
|
|
82
76
|
const cwd = process.cwd();
|
|
83
77
|
const files = await readdir(cwd);
|
|
@@ -119,7 +113,7 @@ export async function plan() {
|
|
|
119
113
|
console.log('--- twin plan ---');
|
|
120
114
|
console.log('Generating tasks that match your taste...\n');
|
|
121
115
|
|
|
122
|
-
const raw = await callLLM(
|
|
116
|
+
const raw = await callLLM(TASK_SYSTEM_PROMPT, userMessage);
|
|
123
117
|
|
|
124
118
|
// Parse structured output
|
|
125
119
|
let prd;
|
|
@@ -136,6 +130,19 @@ export async function plan() {
|
|
|
136
130
|
prd.project = basename(process.cwd());
|
|
137
131
|
}
|
|
138
132
|
|
|
133
|
+
// Merge with existing stories if prd.json already exists
|
|
134
|
+
if (existingPrd) {
|
|
135
|
+
try {
|
|
136
|
+
const existing = JSON.parse(existingPrd);
|
|
137
|
+
const existingStories = existing.userStories || [];
|
|
138
|
+
prd.userStories = [...existingStories, ...prd.userStories];
|
|
139
|
+
if (!prd.project && existing.project) prd.project = existing.project;
|
|
140
|
+
if (!prd.description && existing.description) prd.description = existing.description;
|
|
141
|
+
} catch {
|
|
142
|
+
// If existing prd.json is malformed, just use the new one
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
139
146
|
// Write prd.json
|
|
140
147
|
await writeFile(prdPath, JSON.stringify(prd, null, 2) + '\n', 'utf-8');
|
|
141
148
|
|