@damper/cli 0.6.12 → 0.6.14

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.
@@ -57,6 +57,7 @@ export async function startCommand(options) {
57
57
  let taskId;
58
58
  let taskTitle;
59
59
  let isResume = false;
60
+ let isNewTask = false;
60
61
  let worktreePath;
61
62
  let forceTakeover = options.force || false;
62
63
  if (options.taskId) {
@@ -89,6 +90,7 @@ export async function startCommand(options) {
89
90
  taskId = result.task.id;
90
91
  taskTitle = result.task.title;
91
92
  isResume = result.isResume;
93
+ isNewTask = result.isNewTask || false;
92
94
  forceTakeover = result.forceTakeover || options.force || false;
93
95
  if (result.worktree) {
94
96
  worktreePath = result.worktree.path;
@@ -167,5 +169,6 @@ export async function startCommand(options) {
167
169
  taskId: result.taskId,
168
170
  apiKey: result.apiKey,
169
171
  projectRoot,
172
+ isNewTask,
170
173
  });
171
174
  }
@@ -44,6 +44,7 @@ export declare function postTaskFlow(options: {
44
44
  taskId: string;
45
45
  apiKey: string;
46
46
  projectRoot: string;
47
+ isNewTask?: boolean;
47
48
  }): Promise<void>;
48
49
  /**
49
50
  * Check if Claude Code CLI is installed
@@ -139,16 +139,14 @@ export async function launchClaude(options) {
139
139
  });
140
140
  child.on('close', () => resolve());
141
141
  });
142
- // Post-task flow
143
- console.log(pc.dim('\n─────────────────────────────────────────'));
144
- console.log(pc.bold('\nClaude session ended.\n'));
142
+ console.log(pc.dim('\n─────────────────────────────────────────\n'));
145
143
  return { cwd, taskId, apiKey };
146
144
  }
147
145
  /**
148
146
  * Post-task actions after Claude exits
149
147
  */
150
148
  export async function postTaskFlow(options) {
151
- const { cwd, taskId, apiKey, projectRoot } = options;
149
+ const { cwd, taskId, apiKey, projectRoot, isNewTask } = options;
152
150
  const { confirm, select } = await import('@inquirer/prompts');
153
151
  const { createDamperApi } = await import('./damper-api.js');
154
152
  const { removeWorktreeDir } = await import('./worktree.js');
@@ -156,15 +154,34 @@ export async function postTaskFlow(options) {
156
154
  // Check task status from Damper
157
155
  let taskStatus;
158
156
  let taskTitle;
157
+ let hasCommits = false;
159
158
  try {
160
159
  const api = createDamperApi(apiKey);
161
160
  const task = await api.getTask(taskId);
162
161
  taskStatus = task.status;
163
162
  taskTitle = task.title;
163
+ hasCommits = (task.commits?.length ?? 0) > 0;
164
164
  }
165
165
  catch {
166
166
  console.log(pc.yellow('Could not fetch task status from Damper.'));
167
167
  }
168
+ // Clean up CLI-generated modifications to tracked files before checking git status
169
+ // The CLI appends a "## Current Task:" section to CLAUDE.md which is never meant to be committed
170
+ try {
171
+ const claudeMdPath = path.join(cwd, 'CLAUDE.md');
172
+ if (fs.existsSync(claudeMdPath)) {
173
+ const content = fs.readFileSync(claudeMdPath, 'utf-8');
174
+ const marker = '## Current Task:';
175
+ const markerIndex = content.indexOf(marker);
176
+ if (markerIndex !== -1) {
177
+ const restored = content.slice(0, markerIndex).trimEnd() + '\n';
178
+ fs.writeFileSync(claudeMdPath, restored, 'utf-8');
179
+ }
180
+ }
181
+ }
182
+ catch {
183
+ // Ignore cleanup errors
184
+ }
168
185
  // Check git status
169
186
  let hasUnpushedCommits = false;
170
187
  let hasUncommittedChanges = false;
@@ -220,6 +237,7 @@ export async function postTaskFlow(options) {
220
237
  console.log();
221
238
  // Offer actions based on state
222
239
  const hasChanges = hasUnpushedCommits || hasUncommittedChanges;
240
+ let worktreeRemoved = false;
223
241
  if (hasChanges) {
224
242
  if (hasUncommittedChanges) {
225
243
  console.log(pc.yellow('⚠ There are uncommitted changes. You may want to commit them first.\n'));
@@ -247,9 +265,7 @@ export async function postTaskFlow(options) {
247
265
  await execa('git', ['checkout', 'main'], { cwd: projectRoot, stdio: 'pipe' });
248
266
  // Merge the feature branch
249
267
  await execa('git', ['merge', currentBranch, '--no-edit'], { cwd: projectRoot, stdio: 'inherit' });
250
- // Push main
251
- await execa('git', ['push', 'origin', 'main'], { cwd: projectRoot, stdio: 'inherit' });
252
- console.log(pc.green('✓ Merged to main and pushed\n'));
268
+ console.log(pc.green('✓ Merged to main locally\n'));
253
269
  }
254
270
  catch (err) {
255
271
  console.log(pc.red('Failed to merge. You may need to resolve conflicts manually.\n'));
@@ -286,6 +302,7 @@ export async function postTaskFlow(options) {
286
302
  if (shouldCleanup) {
287
303
  try {
288
304
  await removeWorktreeDir(cwd, projectRoot);
305
+ worktreeRemoved = true;
289
306
  console.log(pc.green('✓ Worktree and branch removed\n'));
290
307
  }
291
308
  catch (err) {
@@ -314,6 +331,7 @@ export async function postTaskFlow(options) {
314
331
  if (shouldRemove) {
315
332
  try {
316
333
  await removeWorktreeDir(cwd, projectRoot);
334
+ worktreeRemoved = true;
317
335
  console.log(pc.green('✓ Worktree and branch removed\n'));
318
336
  }
319
337
  catch (err) {
@@ -321,10 +339,44 @@ export async function postTaskFlow(options) {
321
339
  console.log(pc.red(`Failed to remove worktree: ${error.message}\n`));
322
340
  }
323
341
  }
324
- else {
325
- console.log(pc.dim('\nWorktree kept. Run `npx @damper/cli cleanup` to remove later.\n'));
342
+ }
343
+ // Offer to delete CLI-created tasks with no work done
344
+ let taskDeleted = false;
345
+ if (isNewTask && !hasCommits && taskStatus !== 'done') {
346
+ const shouldDelete = await confirm({
347
+ message: 'No work was committed. Remove task from backlog?',
348
+ default: true,
349
+ });
350
+ if (shouldDelete) {
351
+ try {
352
+ const api = createDamperApi(apiKey);
353
+ await api.deleteTask(taskId);
354
+ taskDeleted = true;
355
+ console.log(pc.green('✓ Task removed from backlog\n'));
356
+ }
357
+ catch (err) {
358
+ const error = err;
359
+ console.log(pc.yellow(`Could not delete task: ${error.message}\n`));
360
+ }
361
+ }
362
+ }
363
+ // Final summary
364
+ console.log(pc.dim('─────────────────────────────────────────'));
365
+ if (taskDeleted) {
366
+ console.log(pc.dim(`\n Task removed. No changes saved.\n`));
367
+ }
368
+ else {
369
+ const statusColor = taskStatus === 'done' ? pc.green : taskStatus === 'in_progress' ? pc.yellow : pc.dim;
370
+ const statusLabel = taskStatus || 'unknown';
371
+ console.log(pc.bold(`\n #${shortIdRaw(taskId)} ${taskTitle || 'Unknown task'}`) + ` ${statusColor(statusLabel)}`);
372
+ if (!worktreeRemoved) {
373
+ console.log(pc.dim(` ${cwd}`));
374
+ console.log(`\n To continue, run:`);
375
+ console.log(pc.cyan(` cd ${cwd} && claude -c`));
376
+ console.log(pc.dim(` or: npx @damper/cli --task ${taskId}`));
326
377
  }
327
378
  }
379
+ console.log();
328
380
  }
329
381
  /**
330
382
  * Check if Claude Code CLI is installed
@@ -170,7 +170,11 @@ export declare class DamperApi {
170
170
  }>;
171
171
  getModule(name: string): Promise<Module>;
172
172
  getAgentInstructions(format?: 'markdown' | 'section'): Promise<AgentInstructions>;
173
- createTask(title: string, type?: 'bug' | 'feature' | 'improvement' | 'task'): Promise<Task>;
173
+ createTask(title: string, type?: 'bug' | 'feature' | 'improvement' | 'task', description?: string): Promise<Task>;
174
+ deleteTask(taskId: string): Promise<{
175
+ id: string;
176
+ deleted: boolean;
177
+ }>;
174
178
  getLinkedFeedback(taskId: string): Promise<Array<{
175
179
  id: string;
176
180
  title: string;
@@ -152,8 +152,12 @@ This project uses Damper MCP for task tracking. **You MUST follow this workflow.
152
152
  };
153
153
  }
154
154
  // Create task
155
- async createTask(title, type = 'task') {
156
- return this.request('POST', '/api/agent/tasks', { title, type, status: 'planned' });
155
+ async createTask(title, type = 'task', description) {
156
+ return this.request('POST', '/api/agent/tasks', { title, type, status: 'planned', description });
157
+ }
158
+ // Delete task (only planned, no commits)
159
+ async deleteTask(taskId) {
160
+ return this.request('DELETE', `/api/agent/tasks/${taskId}`);
157
161
  }
158
162
  // Feedback
159
163
  async getLinkedFeedback(taskId) {
@@ -22,7 +22,9 @@ export declare function formatSubtaskProgress(progress: {
22
22
  export declare function relativeTime(date: string | Date | null | undefined): string;
23
23
  /** Returns a picocolors function for the given status */
24
24
  export declare function statusColor(status: string): (s: string) => string;
25
- /** Box-drawing section header: `┌─ Label ───────────────┐` */
25
+ /** Terminal width (min 80) */
26
+ export declare function getTerminalWidth(): number;
27
+ /** Section header spanning terminal width: `── Label ───────────` */
26
28
  export declare function sectionHeader(label: string, width?: number): string;
27
29
  /** Compact one-liner: `🔴 ✨ #a1b2c3d4 Title ▪ L` */
28
30
  export declare function formatTaskLine(task: Task): string;
package/dist/ui/format.js CHANGED
@@ -93,11 +93,16 @@ export function statusColor(status) {
93
93
  default: return pc.dim;
94
94
  }
95
95
  }
96
- /** Box-drawing section header: `┌─ Label ───────────────┐` */
97
- export function sectionHeader(label, width = 40) {
98
- const inner = `\u2500 ${label} `;
99
- const remaining = Math.max(0, width - inner.length - 2); // 2 for ┌ and ┐
100
- return `\u250C${inner}${'\u2500'.repeat(remaining)}\u2510`;
96
+ /** Terminal width (min 80) */
97
+ export function getTerminalWidth() {
98
+ return Math.max(80, process.stdout.columns || 80);
99
+ }
100
+ /** Section header spanning terminal width: `── Label ───────────` */
101
+ export function sectionHeader(label, width) {
102
+ const w = width ?? getTerminalWidth();
103
+ const prefix = `── ${label} `;
104
+ const remaining = Math.max(0, w - prefix.length);
105
+ return pc.dim(prefix + '─'.repeat(remaining));
101
106
  }
102
107
  /** Compact one-liner: `🔴 ✨ #a1b2c3d4 Title ▪ L` */
103
108
  export function formatTaskLine(task) {
@@ -11,6 +11,7 @@ interface TaskPickerResult {
11
11
  worktree?: WorktreeState;
12
12
  isResume: boolean;
13
13
  forceTakeover?: boolean;
14
+ isNewTask?: boolean;
14
15
  }
15
16
  export declare function pickTask(options: TaskPickerOptions): Promise<TaskPickerResult | null>;
16
17
  export {};
@@ -1,38 +1,81 @@
1
1
  import { select, confirm, input, Separator } from '@inquirer/prompts';
2
2
  import pc from 'picocolors';
3
- import { shortId, shortIdRaw, getTypeIcon, getPriorityIcon, formatEffort, formatTaskMeta, sectionHeader, relativeTime, } from './format.js';
4
- function formatTaskChoice(choice) {
5
- const { task } = choice;
6
- const priorityIcon = getPriorityIcon(task.priority);
7
- const typeIcon = getTypeIcon(task.type);
8
- const id = shortId(task.id);
9
- const effort = formatEffort(task.effort);
10
- const firstLine = `${priorityIcon}${typeIcon} ${id} ${task.title}${effort ? ` ${effort}` : ''}`;
3
+ import { shortId, shortIdRaw, getTypeIcon, getPriorityIcon, formatEffort, formatSubtaskProgress, sectionHeader, relativeTime, getTerminalWidth, } from './format.js';
4
+ // Layout constants (terminal column widths)
5
+ const CURSOR_WIDTH = 2; // inquirer select prefix (❯ or )
6
+ const PRIORITY_WIDTH = 3; // emoji(2) + space(1), or 3 spaces
7
+ const TYPE_WIDTH = 2; // emoji(2)
8
+ const GAP1 = 1; // space after type
9
+ const ID_WIDTH = 9; // # + 8 chars
10
+ const GAP2 = 2; // spaces after ID
11
+ const GAP3 = 2; // spaces between title and meta
12
+ const META_RESERVE = 16; // max width for right-side meta
13
+ const MIN_TITLE = 20; // minimum title column width
14
+ const LEFT_FIXED = CURSOR_WIDTH + PRIORITY_WIDTH + TYPE_WIDTH + GAP1 + ID_WIDTH + GAP2; // 19
15
+ const INDENT_WIDTH = PRIORITY_WIDTH + TYPE_WIDTH + GAP1 + ID_WIDTH + GAP2; // 17 (without cursor)
16
+ function getTitleWidth() {
17
+ const termWidth = getTerminalWidth();
18
+ return Math.max(MIN_TITLE, termWidth - LEFT_FIXED - GAP3 - META_RESERVE);
19
+ }
20
+ function formatTableRow(task, titleWidth) {
21
+ // Priority: always 3 terminal cols
22
+ const hasPriority = task.priority === 'high' || task.priority === 'medium';
23
+ const priorityStr = hasPriority ? getPriorityIcon(task.priority) : ' ';
24
+ // Type icon: 2 terminal cols
25
+ const typeStr = getTypeIcon(task.type);
26
+ // ID: 9 visible chars
27
+ const idStr = shortId(task.id);
28
+ // Title: padded or truncated to fixed width
29
+ const titleStr = task.title.length > titleWidth
30
+ ? task.title.slice(0, titleWidth - 1) + '\u2026'
31
+ : task.title + ' '.repeat(titleWidth - task.title.length);
32
+ // Right-side meta (compact)
33
+ const metaParts = [];
34
+ if (task.effort)
35
+ metaParts.push(formatEffort(task.effort));
36
+ if (task.feedbackCount)
37
+ metaParts.push(pc.dim(`\u{1F4AC}${task.feedbackCount}`));
38
+ if (task.subtaskProgress?.total)
39
+ metaParts.push(formatSubtaskProgress(task.subtaskProgress));
40
+ const meta = metaParts.join(' ');
41
+ return `${priorityStr}${typeStr} ${idStr} ${titleStr} ${meta}`;
42
+ }
43
+ function formatTaskChoice(choice, titleWidth) {
44
+ const firstLine = formatTableRow(choice.task, titleWidth);
45
+ const indent = ' '.repeat(INDENT_WIDTH);
11
46
  if (choice.type === 'in_progress') {
12
47
  const worktreeName = choice.worktree.path.split('/').pop() || choice.worktree.path;
13
48
  const time = relativeTime(choice.worktree.createdAt);
14
49
  const metaParts = [worktreeName];
15
50
  if (time)
16
51
  metaParts.push(time);
17
- let description = `${firstLine}\n ${pc.dim(metaParts.join(' \u00B7 '))}`;
52
+ let description = `${firstLine}\n${indent}${pc.dim(metaParts.join(' \u00B7 '))}`;
18
53
  if (choice.lastNote) {
19
- const note = choice.lastNote.length > 60 ? choice.lastNote.slice(0, 60) + '...' : choice.lastNote;
20
- description += `\n ${pc.dim(`Last: "${note}"`)}`;
54
+ const maxNoteWidth = titleWidth;
55
+ const note = choice.lastNote.length > maxNoteWidth ? choice.lastNote.slice(0, maxNoteWidth - 3) + '...' : choice.lastNote;
56
+ description += `\n${indent}${pc.dim(`Last: "${note}"`)}`;
21
57
  }
22
58
  return description;
23
59
  }
24
60
  if (choice.type === 'locked') {
25
- return `${firstLine}\n ${pc.yellow(`locked by ${choice.lockedBy}`)}`;
61
+ return `${firstLine}\n${indent}${pc.yellow(`locked by ${choice.lockedBy}`)}`;
26
62
  }
27
- // Available task
28
- const meta = formatTaskMeta(task);
29
- if (meta) {
30
- return `${firstLine}\n ${meta}`;
63
+ // Available task - add secondary meta line if there's useful info
64
+ const secondaryParts = [];
65
+ if (choice.task.quarter)
66
+ secondaryParts.push(choice.task.quarter);
67
+ if (choice.task.hasImplementationPlan)
68
+ secondaryParts.push('has plan');
69
+ if (choice.task.labels?.length)
70
+ secondaryParts.push(choice.task.labels.join(' \u00B7 '));
71
+ if (secondaryParts.length > 0) {
72
+ return `${firstLine}\n${indent}${pc.dim(secondaryParts.join(` \u00B7 `))}`;
31
73
  }
32
74
  return firstLine;
33
75
  }
34
76
  export async function pickTask(options) {
35
77
  const { api, worktrees, typeFilter, statusFilter } = options;
78
+ const titleWidth = getTitleWidth();
36
79
  // Fetch tasks from Damper
37
80
  const { tasks, project } = await api.listTasks({
38
81
  status: statusFilter || 'all',
@@ -50,8 +93,6 @@ export async function pickTask(options) {
50
93
  const task = tasks.find(t => t.id === worktree.taskId);
51
94
  if (task) {
52
95
  worktreeTaskIds.add(task.id);
53
- // Get last note for display (we'd need to fetch task detail for this)
54
- // For now, we'll leave it undefined - the API doesn't include notes in list
55
96
  inProgressChoices.push({
56
97
  type: 'in_progress',
57
98
  task,
@@ -67,7 +108,6 @@ export async function pickTask(options) {
67
108
  continue;
68
109
  const taskAny = task;
69
110
  if (task.status === 'in_progress' && taskAny.lockedBy) {
70
- // Task is locked by someone else
71
111
  lockedChoices.push({
72
112
  type: 'locked',
73
113
  task,
@@ -83,7 +123,7 @@ export async function pickTask(options) {
83
123
  choices.push(new Separator(`\n${sectionHeader(`In Progress (${inProgressChoices.length})`)}`));
84
124
  for (const choice of inProgressChoices) {
85
125
  choices.push({
86
- name: formatTaskChoice(choice),
126
+ name: formatTaskChoice(choice, titleWidth),
87
127
  value: choice,
88
128
  });
89
129
  }
@@ -92,7 +132,7 @@ export async function pickTask(options) {
92
132
  choices.push(new Separator(`\n${sectionHeader(`Available (${availableChoices.length})`)}`));
93
133
  for (const choice of availableChoices) {
94
134
  choices.push({
95
- name: formatTaskChoice(choice),
135
+ name: formatTaskChoice(choice, titleWidth),
96
136
  value: choice,
97
137
  });
98
138
  }
@@ -101,7 +141,7 @@ export async function pickTask(options) {
101
141
  choices.push(new Separator(`\n${sectionHeader(`Locked (${lockedChoices.length})`)}`));
102
142
  for (const choice of lockedChoices) {
103
143
  choices.push({
104
- name: formatTaskChoice(choice),
144
+ name: formatTaskChoice(choice, titleWidth),
105
145
  value: choice,
106
146
  });
107
147
  }
@@ -158,9 +198,9 @@ export async function pickTask(options) {
158
198
  };
159
199
  }
160
200
  async function handleCreateNewTask(api) {
161
- const title = await input({
162
- message: 'Task title:',
163
- validate: (value) => value.trim().length > 0 || 'Title is required',
201
+ const instructions = await input({
202
+ message: 'What needs to be done?',
203
+ validate: (value) => value.trim().length > 0 || 'Instructions are required',
164
204
  });
165
205
  const type = await select({
166
206
  message: 'Task type:',
@@ -171,11 +211,19 @@ async function handleCreateNewTask(api) {
171
211
  { name: 'Task', value: 'task' },
172
212
  ],
173
213
  });
214
+ // Use instructions as description; derive a placeholder title
215
+ const trimmed = instructions.trim();
216
+ const maxTitleLen = 60;
217
+ const placeholderTitle = trimmed.length <= maxTitleLen
218
+ ? trimmed
219
+ : trimmed.slice(0, trimmed.lastIndexOf(' ', maxTitleLen) || maxTitleLen) + '…';
174
220
  console.log(pc.dim('\nCreating task in Damper...'));
175
- const task = await api.createTask(title.trim(), type);
221
+ const task = await api.createTask(placeholderTitle, type, trimmed);
176
222
  console.log(pc.green(`✓ Created task #${shortIdRaw(task.id)}: ${task.title}`));
223
+ console.log(pc.dim(' The agent will refine the title after planning.'));
177
224
  return {
178
225
  task,
179
226
  isResume: false,
227
+ isNewTask: true,
180
228
  };
181
229
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@damper/cli",
3
- "version": "0.6.12",
3
+ "version": "0.6.14",
4
4
  "description": "CLI tool for orchestrating Damper task workflows with Claude Code",
5
5
  "author": "Damper <hello@usedamper.com>",
6
6
  "repository": {