@damper/cli 0.9.20 → 0.10.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.
@@ -1,8 +1,7 @@
1
1
  import { confirm, select } from '@inquirer/prompts';
2
2
  import pc from 'picocolors';
3
3
  import { createDamperApi } from '../services/damper-api.js';
4
- import { getWorktrees, removeWorktree } from '../services/state.js';
5
- import { removeWorktreeDir, getGitRoot } from '../services/worktree.js';
4
+ import { getGitRoot } from '../services/git.js';
6
5
  import { getApiKey } from '../services/config.js';
7
6
  import { shortId, shortIdRaw, getTypeIcon, getPriorityIcon, formatEffort } from '../ui/format.js';
8
7
  export async function releaseCommand() {
@@ -37,33 +36,26 @@ export async function releaseCommand() {
37
36
  console.log(pc.bold(`\nProject: ${project}`));
38
37
  console.log(pc.dim('Select a task to release back to planned status.\n'));
39
38
  // Build choices
40
- const worktrees = getWorktrees();
41
39
  const choices = tasks.map(task => {
42
- const worktree = worktrees.find(w => w.taskId === task.id);
43
40
  const priorityIcon = getPriorityIcon(task.priority);
44
41
  const typeIcon = getTypeIcon(task.type);
45
42
  const id = shortId(task.id);
46
43
  const effort = formatEffort(task.effort);
47
44
  const lockedInfo = task.lockedBy ? pc.dim(` (locked by ${task.lockedBy})`) : '';
48
- const worktreeInfo = worktree ? pc.dim(` [has worktree]`) : '';
49
45
  return {
50
- name: `${priorityIcon}${typeIcon} ${id} ${task.title}${effort ? ` ${effort}` : ''}${lockedInfo}${worktreeInfo}`,
51
- value: { task, worktree },
46
+ name: `${priorityIcon}${typeIcon} ${id} ${task.title}${effort ? ` ${effort}` : ''}${lockedInfo}`,
47
+ value: task,
52
48
  };
53
49
  });
54
- const selected = await select({
50
+ const task = await select({
55
51
  message: 'Select task to release:',
56
52
  choices,
57
53
  });
58
- const { task, worktree } = selected;
59
54
  // Confirm
60
55
  console.log();
61
56
  console.log(pc.yellow(`This will:`));
62
57
  console.log(pc.dim(` • Release the lock on task #${shortIdRaw(task.id)}`));
63
58
  console.log(pc.dim(` • Set task status back to "planned"`));
64
- if (worktree) {
65
- console.log(pc.dim(` • Optionally remove the worktree`));
66
- }
67
59
  console.log();
68
60
  const shouldRelease = await confirm({
69
61
  message: 'Release this task?',
@@ -83,23 +75,5 @@ export async function releaseCommand() {
83
75
  console.log(pc.red(`\nFailed to release task: ${error.message}\n`));
84
76
  return;
85
77
  }
86
- // Offer to remove worktree if exists
87
- if (worktree) {
88
- const shouldRemoveWorktree = await confirm({
89
- message: 'Remove the worktree and branch?',
90
- default: false,
91
- });
92
- if (shouldRemoveWorktree) {
93
- try {
94
- await removeWorktreeDir(worktree.path, worktree.projectRoot);
95
- console.log(pc.green('✓ Worktree and branch removed'));
96
- }
97
- catch (err) {
98
- const error = err;
99
- console.log(pc.yellow(`Could not remove worktree: ${error.message}`));
100
- removeWorktree(task.id); // At least clean up state
101
- }
102
- }
103
- }
104
78
  console.log();
105
79
  }
@@ -55,17 +55,9 @@ export async function setupCommand(options = {}) {
55
55
  if (!projectRoot) {
56
56
  console.log(pc.yellow('\n⚠️ Not in a git repository'));
57
57
  console.log(pc.dim(' Run this command from within a project to configure it.\n'));
58
- // Still offer to set up global MCP if not configured
58
+ // Can't set up MCP without API key (HTTP transport needs it in headers)
59
59
  if (!mcpConfigured) {
60
- const shouldSetupMcp = await confirm({
61
- message: 'Set up Damper MCP globally (without API key)?',
62
- default: true,
63
- });
64
- if (shouldSetupMcp) {
65
- configureDamperMcp();
66
- console.log(pc.green('\n✓ Damper MCP configured globally'));
67
- console.log(pc.dim(' Run setup again from a project directory to add an API key.\n'));
68
- }
60
+ console.log(pc.dim('\n Run setup from a project directory to configure MCP with an API key.\n'));
69
61
  }
70
62
  return;
71
63
  }
@@ -142,7 +134,7 @@ export async function setupCommand(options = {}) {
142
134
  console.log(pc.green(`✓ Saved API key to ${getProjectConfigPath(projectRoot)}`));
143
135
  // Set up global MCP if not configured
144
136
  if (!mcpConfigured) {
145
- configureDamperMcp();
137
+ configureDamperMcp(apiKey);
146
138
  console.log(pc.green('✓ Configured Damper MCP globally'));
147
139
  }
148
140
  // Remind about .gitignore
@@ -1,11 +1,8 @@
1
- import * as fs from 'node:fs';
2
1
  import pc from 'picocolors';
3
2
  import { createDamperApi } from '../services/damper-api.js';
4
- import { createWorktree, getMainProjectRoot, removeWorktreeDir } from '../services/worktree.js';
5
- import { bootstrapContext, refreshContext } from '../services/context-bootstrap.js';
3
+ import { getGitRoot } from '../services/git.js';
6
4
  import { pickTask } from '../ui/task-picker.js';
7
5
  import { launchClaude, launchClaudeForReview, postTaskFlow, isClaudeInstalled, isDamperMcpConfigured, configureDamperMcp } from '../services/claude.js';
8
- import { getWorktreesForProject, cleanupStaleWorktrees } from '../services/state.js';
9
6
  import { getApiKey, isProjectConfigured, getProjectConfigPath } from '../services/config.js';
10
7
  import { shortIdRaw } from '../ui/format.js';
11
8
  export async function startCommand(options) {
@@ -16,10 +13,10 @@ export async function startCommand(options) {
16
13
  console.log(pc.dim('Install it with: npm install -g @anthropic-ai/claude-code\n'));
17
14
  process.exit(1);
18
15
  }
19
- // Get project root (main repo, not a worktree)
16
+ // Get project root
20
17
  let projectRoot;
21
18
  try {
22
- projectRoot = await getMainProjectRoot(process.cwd());
19
+ projectRoot = await getGitRoot(process.cwd());
23
20
  }
24
21
  catch {
25
22
  console.log(pc.red('\nError: Not in a git repository.'));
@@ -39,37 +36,27 @@ export async function startCommand(options) {
39
36
  }
40
37
  process.exit(1);
41
38
  }
42
- // Ensure MCP is configured globally (without key - key comes from env)
39
+ // Ensure MCP is configured globally
43
40
  if (!isDamperMcpConfigured()) {
44
41
  console.log(pc.dim('Configuring Damper MCP...'));
45
- configureDamperMcp();
42
+ configureDamperMcp(apiKey);
46
43
  console.log(pc.green('✓ Damper MCP configured'));
47
44
  }
48
45
  // Create API client with the project's key
49
46
  const api = createDamperApi(apiKey);
50
- // Clean up stale worktrees
51
- const stale = cleanupStaleWorktrees();
52
- if (stale.length > 0) {
53
- console.log(pc.dim(`Cleaned up ${stale.length} stale worktree reference(s)`));
54
- }
55
- // Get existing worktrees for this project
56
- const worktrees = getWorktreesForProject(projectRoot);
57
47
  let taskId;
58
48
  let taskTitle;
59
49
  let isResume = false;
60
50
  let isNewTask = false;
61
- let worktreePath;
62
51
  let forceTakeover = options.force || false;
63
52
  if (options.taskId) {
64
53
  // Direct task selection
65
54
  taskId = options.taskId;
66
55
  const task = await api.getTask(taskId);
67
56
  taskTitle = task.title;
68
- // Check if we have an existing worktree for this task
69
- const existingWorktree = worktrees.find(w => w.taskId === taskId);
70
- if (existingWorktree && fs.existsSync(existingWorktree.path)) {
57
+ // Check if task is already in progress (resume)
58
+ if (task.status === 'in_progress') {
71
59
  isResume = true;
72
- worktreePath = existingWorktree.path;
73
60
  console.log(pc.cyan(`\nResuming task #${shortIdRaw(taskId)}: ${taskTitle}`));
74
61
  }
75
62
  else {
@@ -79,11 +66,8 @@ export async function startCommand(options) {
79
66
  else {
80
67
  // Interactive task picker with review loop
81
68
  while (true) {
82
- // Re-fetch worktrees each iteration (handles cleanup between reviews)
83
- const currentWorktrees = getWorktreesForProject(projectRoot);
84
69
  const result = await pickTask({
85
70
  api,
86
- worktrees: currentWorktrees,
87
71
  typeFilter: options.type,
88
72
  statusFilter: options.status,
89
73
  });
@@ -95,7 +79,6 @@ export async function startCommand(options) {
95
79
  api,
96
80
  apiKey,
97
81
  task: result.task,
98
- worktree: result.worktree,
99
82
  projectRoot,
100
83
  });
101
84
  continue;
@@ -106,16 +89,10 @@ export async function startCommand(options) {
106
89
  isResume = result.isResume;
107
90
  isNewTask = result.isNewTask || false;
108
91
  forceTakeover = result.forceTakeover || options.force || false;
109
- if (result.worktree) {
110
- worktreePath = result.worktree.path;
111
- }
112
92
  break;
113
93
  }
114
94
  }
115
- if (isResume && worktreePath) {
116
- // Resume existing worktree
117
- console.log(pc.green(`\n✓ Resuming: #${shortIdRaw(taskId)} ${taskTitle}`));
118
- console.log(pc.dim(` Worktree: ${worktreePath}`));
95
+ if (isResume) {
119
96
  // Re-lock the task if it was previously released
120
97
  const task = await api.getTask(taskId);
121
98
  if (task.status !== 'in_progress' || !task.lockedBy) {
@@ -134,23 +111,12 @@ export async function startCommand(options) {
134
111
  throw err;
135
112
  }
136
113
  }
137
- // Refresh context with latest from Damper
138
- console.log(pc.dim('\nRefreshing context from Damper...'));
139
- await refreshContext({
140
- api,
141
- taskId,
142
- worktreePath,
143
- yolo: options.yolo,
144
- });
145
- console.log(pc.green('✓ Updated TASK_CONTEXT.md with latest notes'));
146
114
  }
147
115
  else {
148
116
  // New task - lock it first
149
117
  console.log(pc.dim('\nLocking task in Damper...'));
150
- let completionChecklist;
151
118
  try {
152
- const startResult = await api.startTask(taskId, forceTakeover);
153
- completionChecklist = startResult.completionChecklist;
119
+ await api.startTask(taskId, forceTakeover);
154
120
  console.log(pc.green(forceTakeover ? '✓ Task lock taken over' : '✓ Task locked'));
155
121
  }
156
122
  catch (err) {
@@ -162,39 +128,10 @@ export async function startCommand(options) {
162
128
  }
163
129
  throw err;
164
130
  }
165
- // Create worktree
166
- console.log(pc.dim('\nSetting up worktree...'));
167
- const worktreeResult = await createWorktree({
168
- taskId,
169
- taskTitle,
170
- projectRoot,
171
- apiKey,
172
- });
173
- worktreePath = worktreeResult.path;
174
- if (worktreeResult.isNew) {
175
- console.log(pc.green(`✓ Created worktree: ${worktreePath}`));
176
- console.log(pc.dim(` Branch: ${worktreeResult.branch}`));
177
- }
178
- else {
179
- console.log(pc.green(`✓ Using existing worktree: ${worktreePath}`));
180
- }
181
- // Bootstrap context
182
- console.log(pc.dim('\nBootstrapping context from Damper...'));
183
- const bootstrapResult = await bootstrapContext({
184
- api,
185
- taskId,
186
- worktreePath,
187
- yolo: options.yolo,
188
- completionChecklist,
189
- });
190
- console.log(pc.green(`✓ Created ${bootstrapResult.taskContextPath}`));
191
- if (bootstrapResult.claudeMdUpdated) {
192
- console.log(pc.green('✓ Updated CLAUDE.md with task section'));
193
- }
194
131
  }
195
- // Launch Claude with project's API key
132
+ // Launch Claude in project root
196
133
  const result = await launchClaude({
197
- cwd: worktreePath,
134
+ cwd: projectRoot,
198
135
  taskId,
199
136
  taskTitle,
200
137
  apiKey,
@@ -214,14 +151,12 @@ export async function startCommand(options) {
214
151
  * Review a task and attempt to complete it via Claude, then return to the picker.
215
152
  */
216
153
  async function handleReviewAndComplete(options) {
217
- const { api, apiKey, task, worktree, projectRoot } = options;
154
+ const { api, apiKey, task, projectRoot } = options;
218
155
  const { confirm } = await import('@inquirer/prompts');
219
156
  const { execa } = await import('execa');
220
157
  const taskId = task.id;
221
- const cwd = worktree?.path || projectRoot;
222
- const hasWorktree = !!worktree;
223
158
  console.log(pc.cyan(`\nReviewing task #${shortIdRaw(taskId)}: ${task.title}`));
224
- console.log(pc.dim(`Directory: ${cwd}`));
159
+ console.log(pc.dim(`Directory: ${projectRoot}`));
225
160
  // Lock the task
226
161
  console.log(pc.dim('\nLocking task in Damper...'));
227
162
  try {
@@ -235,7 +170,7 @@ async function handleReviewAndComplete(options) {
235
170
  }
236
171
  // Launch Claude for review
237
172
  console.log(pc.dim('\nLaunching Claude for review...\n'));
238
- await launchClaudeForReview({ cwd, apiKey, taskId });
173
+ await launchClaudeForReview({ cwd: projectRoot, apiKey, taskId });
239
174
  console.log(pc.dim('\n─────────────────────────────────────────\n'));
240
175
  // Check task status after Claude exits
241
176
  let taskStatus;
@@ -248,51 +183,34 @@ async function handleReviewAndComplete(options) {
248
183
  }
249
184
  if (taskStatus === 'done') {
250
185
  console.log(pc.green(`✓ Task #${shortIdRaw(taskId)} completed!`));
251
- if (hasWorktree) {
252
- // Check for unpushed commits and offer to push + cleanup
253
- let hasUnpushed = false;
186
+ // Check for unpushed commits and offer to push
187
+ let hasUnpushed = false;
188
+ try {
189
+ const { stdout } = await execa('git', ['log', '@{u}..HEAD', '--oneline'], { cwd: projectRoot, stdio: 'pipe' });
190
+ hasUnpushed = stdout.trim().length > 0;
191
+ }
192
+ catch {
254
193
  try {
255
- const { stdout } = await execa('git', ['log', '@{u}..HEAD', '--oneline'], { cwd, stdio: 'pipe' });
194
+ const { stdout } = await execa('git', ['log', 'origin/main..HEAD', '--oneline'], { cwd: projectRoot, stdio: 'pipe' });
256
195
  hasUnpushed = stdout.trim().length > 0;
257
196
  }
258
197
  catch {
259
- try {
260
- const { stdout } = await execa('git', ['log', 'origin/main..HEAD', '--oneline'], { cwd, stdio: 'pipe' });
261
- hasUnpushed = stdout.trim().length > 0;
262
- }
263
- catch {
264
- // Assume there might be commits
265
- hasUnpushed = true;
266
- }
198
+ hasUnpushed = true;
267
199
  }
268
- if (hasUnpushed) {
269
- const shouldPush = await confirm({
270
- message: 'Push unpushed commits?',
271
- default: true,
272
- });
273
- if (shouldPush) {
274
- try {
275
- const { stdout: branch } = await execa('git', ['rev-parse', '--abbrev-ref', 'HEAD'], { cwd, stdio: 'pipe' });
276
- await execa('git', ['push', '-u', 'origin', branch.trim()], { cwd, stdio: 'inherit' });
277
- console.log(pc.green('✓ Pushed to remote'));
278
- }
279
- catch {
280
- console.log(pc.red('Failed to push. You can push manually.'));
281
- }
282
- }
283
- }
284
- const shouldCleanup = await confirm({
285
- message: 'Remove worktree and branch?',
200
+ }
201
+ if (hasUnpushed) {
202
+ const shouldPush = await confirm({
203
+ message: 'Push unpushed commits?',
286
204
  default: true,
287
205
  });
288
- if (shouldCleanup) {
206
+ if (shouldPush) {
289
207
  try {
290
- await removeWorktreeDir(cwd, projectRoot);
291
- console.log(pc.green(' Worktree and branch removed'));
208
+ const { stdout: branch } = await execa('git', ['rev-parse', '--abbrev-ref', 'HEAD'], { cwd: projectRoot, stdio: 'pipe' });
209
+ await execa('git', ['push', '-u', 'origin', branch.trim()], { cwd: projectRoot, stdio: 'inherit' });
210
+ console.log(pc.green('✓ Pushed to remote'));
292
211
  }
293
- catch (err) {
294
- const error = err;
295
- console.log(pc.red(`Failed to remove worktree: ${error.message}`));
212
+ catch {
213
+ console.log(pc.red('Failed to push. You can push manually.'));
296
214
  }
297
215
  }
298
216
  }
package/dist/index.js CHANGED
@@ -1,8 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import pc from 'picocolors';
3
3
  import { startCommand } from './commands/start.js';
4
- import { statusCommand } from './commands/status.js';
5
- import { cleanupCommand } from './commands/cleanup.js';
6
4
  import { setupCommand } from './commands/setup.js';
7
5
  import { releaseCommand } from './commands/release.js';
8
6
  import { createRequire } from 'node:module';
@@ -15,8 +13,6 @@ ${pc.bold('@damper/cli')} - Agent orchestration for Damper tasks
15
13
  ${pc.bold('Usage:')}
16
14
  npx @damper/cli Start working on a task (interactive picker)
17
15
  npx @damper/cli setup Configure Damper MCP and API key
18
- npx @damper/cli status Show all in-progress worktrees
19
- npx @damper/cli cleanup Remove worktrees (completed, abandoned, or in-progress)
20
16
  npx @damper/cli release Release a task back to planned status
21
17
 
22
18
  ${pc.bold('Options:')}
@@ -37,15 +33,13 @@ ${pc.bold('Examples:')}
37
33
  npx @damper/cli # Interactive task picker
38
34
  npx @damper/cli --task 42 # Start specific task
39
35
  npx @damper/cli --type bug # Show only bugs
40
- npx @damper/cli status # List all worktrees
41
- npx @damper/cli cleanup # Clean up completed tasks
42
36
 
43
37
  ${pc.bold('Workflow:')}
44
38
  1. CLI picks task from Damper roadmap
45
- 2. Creates isolated git worktree
46
- 3. Bootstraps context (TASK_CONTEXT.md)
47
- 4. Launches Claude Code
48
- 5. Claude handles task lifecycle via MCP
39
+ 2. Locks task via Damper API
40
+ 3. Launches Claude Code with MCP context
41
+ 4. Claude handles task lifecycle via MCP
42
+ 5. Post-task: push, PR, merge, changelog
49
43
 
50
44
  ${pc.dim('More info: https://usedamper.com/docs/cli')}
51
45
  `);
@@ -114,12 +108,6 @@ async function main() {
114
108
  case 'setup':
115
109
  await setupCommand(setupOptions);
116
110
  break;
117
- case 'status':
118
- await statusCommand();
119
- break;
120
- case 'cleanup':
121
- await cleanupCommand();
122
- break;
123
111
  case 'release':
124
112
  await releaseCommand();
125
113
  break;
@@ -1,7 +1,7 @@
1
- interface McpServerConfig {
2
- command: string;
3
- args?: string[];
4
- env?: Record<string, string>;
1
+ interface McpServerConfigHttp {
2
+ type: 'http';
3
+ url: string;
4
+ headers?: Record<string, string>;
5
5
  }
6
6
  /**
7
7
  * Check if Damper MCP is configured in Claude settings
@@ -13,15 +13,14 @@ export declare function isDamperMcpConfigured(): boolean;
13
13
  */
14
14
  export declare function getConfiguredApiKey(): string | undefined;
15
15
  /**
16
- * Get the recommended MCP configuration
17
- * Note: API key is passed via environment when launching Claude, not stored in config
16
+ * Get the recommended MCP configuration (hosted Streamable HTTP)
18
17
  */
19
- export declare function getDamperMcpConfig(): McpServerConfig;
18
+ export declare function getDamperMcpConfig(apiKey?: string): McpServerConfigHttp;
20
19
  /**
21
20
  * Configure Damper MCP in Claude settings (global)
22
- * Note: API key is NOT stored here - it's passed via environment when launching Claude
21
+ * With HTTP transport, the API key is stored in the config headers.
23
22
  */
24
- export declare function configureDamperMcp(): void;
23
+ export declare function configureDamperMcp(apiKey?: string): void;
25
24
  /**
26
25
  * Launch Claude Code in a directory
27
26
  */