@damper/cli 0.9.4 → 0.9.6

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.
@@ -174,12 +174,14 @@ export async function postTaskFlow(options) {
174
174
  let taskStatus;
175
175
  let taskTitle;
176
176
  let hasCommits = false;
177
+ let isPublicTask = false;
177
178
  try {
178
179
  const api = createDamperApi(apiKey);
179
180
  const task = await api.getTask(taskId);
180
181
  taskStatus = task.status;
181
182
  taskTitle = task.title;
182
183
  hasCommits = (task.commits?.length ?? 0) > 0;
184
+ isPublicTask = !!task.publicUrl;
183
185
  }
184
186
  catch {
185
187
  console.log(pc.yellow('Could not fetch task status from Damper.'));
@@ -254,6 +256,10 @@ export async function postTaskFlow(options) {
254
256
  console.log(pc.yellow('⚠ Unpushed commits detected'));
255
257
  }
256
258
  console.log();
259
+ // Offer to add to changelog if task is completed
260
+ if (taskStatus === 'done') {
261
+ await offerChangelogFlow({ taskId, taskTitle, apiKey, isPublic: isPublicTask, confirm, select });
262
+ }
257
263
  // Offer actions based on state
258
264
  const hasChanges = hasUnpushedCommits || hasUncommittedChanges;
259
265
  let worktreeRemoved = false;
@@ -441,6 +447,67 @@ export async function postTaskFlow(options) {
441
447
  }
442
448
  console.log();
443
449
  }
450
+ /**
451
+ * Ask user if they want to add the completed task to a changelog
452
+ */
453
+ async function offerChangelogFlow(options) {
454
+ const { taskId, taskTitle, apiKey, isPublic, confirm, select } = options;
455
+ const { input } = await import('@inquirer/prompts');
456
+ const addToChangelog = await confirm({
457
+ message: 'Add this task to a changelog?',
458
+ default: isPublic,
459
+ });
460
+ if (!addToChangelog)
461
+ return;
462
+ try {
463
+ const { createDamperApi } = await import('./damper-api.js');
464
+ const api = createDamperApi(apiKey);
465
+ // Fetch draft changelogs
466
+ const { changelogs } = await api.listChangelogs({ status: 'draft', limit: 10 });
467
+ let changelogId;
468
+ if (changelogs.length > 0) {
469
+ const choice = await select({
470
+ message: 'Which changelog?',
471
+ choices: [
472
+ ...changelogs.map((cl) => ({
473
+ name: `${cl.title}${cl.version ? ` (${cl.version})` : ''} — ${cl.roadmapItemCount} item${cl.roadmapItemCount !== 1 ? 's' : ''}`,
474
+ value: cl.id,
475
+ })),
476
+ { name: 'Create new changelog', value: '__new__' },
477
+ ],
478
+ });
479
+ if (choice === '__new__') {
480
+ const title = await input({
481
+ message: 'Changelog title:',
482
+ default: `Release ${new Date().toISOString().slice(0, 10)}`,
483
+ });
484
+ const result = await api.createChangelog({ title });
485
+ changelogId = result.id;
486
+ console.log(pc.green(`✓ Created changelog: ${title}`));
487
+ }
488
+ else {
489
+ changelogId = choice;
490
+ }
491
+ }
492
+ else {
493
+ // No drafts — create one
494
+ const title = await input({
495
+ message: 'No draft changelogs found. Create one — title:',
496
+ default: `Release ${new Date().toISOString().slice(0, 10)}`,
497
+ });
498
+ const result = await api.createChangelog({ title });
499
+ changelogId = result.id;
500
+ console.log(pc.green(`✓ Created changelog: ${title}`));
501
+ }
502
+ // Add the task
503
+ const result = await api.addToChangelog(changelogId, [taskId]);
504
+ console.log(pc.green(`✓ Added "${taskTitle || taskId}" to changelog (${result.roadmapItemCount} total items)\n`));
505
+ }
506
+ catch (err) {
507
+ const error = err;
508
+ console.log(pc.yellow(`Could not add to changelog: ${error.message}\n`));
509
+ }
510
+ }
444
511
  /**
445
512
  * Launch Claude to review and complete a task
446
513
  */
@@ -37,6 +37,7 @@ export interface TaskDetail extends Task {
37
37
  }>;
38
38
  lockedBy?: string | null;
39
39
  lockedAt?: string | null;
40
+ publicUrl?: string | null;
40
41
  }
41
42
  export interface ContextSection {
42
43
  section: string;
@@ -76,6 +77,16 @@ export interface Module {
76
77
  tags?: string[];
77
78
  updatedAt: string;
78
79
  }
80
+ export interface Changelog {
81
+ id: string;
82
+ title: string;
83
+ version?: string | null;
84
+ date?: string | null;
85
+ status: 'draft' | 'published';
86
+ publishedAt?: string | null;
87
+ roadmapItemCount: number;
88
+ contentPreview: string;
89
+ }
79
90
  export interface AgentInstructions {
80
91
  format: 'markdown' | 'section';
81
92
  content: string;
@@ -182,6 +193,32 @@ export declare class DamperApi {
182
193
  }>;
183
194
  getModule(name: string): Promise<Module>;
184
195
  getAgentInstructions(format?: 'markdown' | 'section'): Promise<AgentInstructions>;
196
+ listChangelogs(filters?: {
197
+ status?: 'draft' | 'published';
198
+ limit?: number;
199
+ sort?: 'newest' | 'oldest';
200
+ }): Promise<{
201
+ changelogs: Changelog[];
202
+ }>;
203
+ createChangelog(data: {
204
+ title: string;
205
+ content?: string;
206
+ version?: string;
207
+ status?: 'draft' | 'published';
208
+ }): Promise<{
209
+ id: string;
210
+ title: string;
211
+ status: string;
212
+ }>;
213
+ addToChangelog(changelogId: string, taskIds: string[]): Promise<{
214
+ id: string;
215
+ title: string;
216
+ roadmapItemCount: number;
217
+ addedItems: Array<{
218
+ id: string;
219
+ title: string;
220
+ }>;
221
+ }>;
185
222
  createTask(title: string, type?: 'bug' | 'feature' | 'improvement' | 'task', description?: string): Promise<Task>;
186
223
  deleteTask(taskId: string): Promise<{
187
224
  id: string;
@@ -170,6 +170,24 @@ This project uses Damper MCP for task tracking. **You MUST follow this workflow.
170
170
  lastModified,
171
171
  };
172
172
  }
173
+ // Changelogs
174
+ async listChangelogs(filters) {
175
+ const params = new URLSearchParams();
176
+ if (filters?.status)
177
+ params.set('status', filters.status);
178
+ if (filters?.limit)
179
+ params.set('limit', String(filters.limit));
180
+ if (filters?.sort)
181
+ params.set('sort', filters.sort);
182
+ const query = params.toString();
183
+ return this.request('GET', `/api/agent/changelogs${query ? `?${query}` : ''}`);
184
+ }
185
+ async createChangelog(data) {
186
+ return this.request('POST', '/api/agent/changelogs', data);
187
+ }
188
+ async addToChangelog(changelogId, taskIds) {
189
+ return this.request('POST', `/api/agent/changelogs/${changelogId}/items`, { taskIds });
190
+ }
173
191
  // Create task
174
192
  async createTask(title, type = 'task', description) {
175
193
  return this.request('POST', '/api/agent/tasks', { title, type, status: 'planned', description });
@@ -260,6 +260,34 @@ export async function pickTask(options) {
260
260
  action,
261
261
  };
262
262
  }
263
+ async function generateTaskTitle(instructions, type) {
264
+ try {
265
+ const { execa } = await import('execa');
266
+ const prompt = [
267
+ `Generate a task title for a ${type} task.`,
268
+ 'Rules:',
269
+ '- Max 60 characters, sentence case, no trailing period',
270
+ '- Verb prefix matching type: bug → "Fix …", feature → "Add …", improvement → "Improve …", task → "Set up …" / "Update …"',
271
+ '- Specific enough to understand without reading a description',
272
+ '- Examples: "Add dark mode support", "Fix login timeout on slow connections", "Improve search result ranking"',
273
+ 'Return ONLY the title, nothing else.',
274
+ '',
275
+ `User input: ${instructions}`,
276
+ ].join('\n');
277
+ const { stdout } = await execa('claude', ['--print', '--model', 'haiku', prompt], {
278
+ stdio: 'pipe',
279
+ timeout: 15000,
280
+ });
281
+ const title = stdout.trim();
282
+ if (title && title.length <= 80 && !title.includes('\n')) {
283
+ return title;
284
+ }
285
+ return null;
286
+ }
287
+ catch {
288
+ return null;
289
+ }
290
+ }
263
291
  async function handleCreateNewTask(api) {
264
292
  const instructions = await input({
265
293
  message: 'What needs to be done?',
@@ -274,16 +302,23 @@ async function handleCreateNewTask(api) {
274
302
  { name: 'Task', value: 'task' },
275
303
  ],
276
304
  });
277
- // Use instructions as description; derive a placeholder title
278
305
  const trimmed = instructions.trim();
279
- const maxTitleLen = 60;
280
- const placeholderTitle = trimmed.length <= maxTitleLen
281
- ? trimmed
282
- : trimmed.slice(0, trimmed.lastIndexOf(' ', maxTitleLen) || maxTitleLen) + '…';
283
- console.log(pc.dim('\nCreating task in Damper...'));
284
- const task = await api.createTask(placeholderTitle, type, trimmed);
306
+ // Generate title with Claude, fall back to truncation
307
+ console.log(pc.dim('\nGenerating title...'));
308
+ const generatedTitle = await generateTaskTitle(trimmed, type);
309
+ let title;
310
+ if (generatedTitle) {
311
+ title = generatedTitle;
312
+ }
313
+ else {
314
+ const maxTitleLen = 60;
315
+ title = trimmed.length <= maxTitleLen
316
+ ? trimmed
317
+ : trimmed.slice(0, trimmed.lastIndexOf(' ', maxTitleLen) || maxTitleLen) + '…';
318
+ }
319
+ console.log(pc.dim('Creating task in Damper...'));
320
+ const task = await api.createTask(title, type, trimmed);
285
321
  console.log(pc.green(`✓ Created task #${shortIdRaw(task.id)}: ${task.title}`));
286
- console.log(pc.dim(' The agent will refine the title after planning.'));
287
322
  return {
288
323
  task,
289
324
  isResume: false,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@damper/cli",
3
- "version": "0.9.4",
3
+ "version": "0.9.6",
4
4
  "description": "CLI tool for orchestrating Damper task workflows with Claude Code",
5
5
  "author": "Damper <hello@usedamper.com>",
6
6
  "repository": {