@damper/cli 0.9.9 → 0.9.11

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.
@@ -4,7 +4,7 @@ import * as os from 'node:os';
4
4
  import { spawn } from 'node:child_process';
5
5
  import { execa } from 'execa';
6
6
  import pc from 'picocolors';
7
- import { shortIdRaw } from '../ui/format.js';
7
+ import { shortIdRaw, setTerminalTitle, clearTerminalTitle, TerminalStatusBar } from '../ui/format.js';
8
8
  const CLAUDE_SETTINGS_DIR = path.join(os.homedir(), '.claude');
9
9
  const CLAUDE_SETTINGS_FILE = path.join(CLAUDE_SETTINGS_DIR, 'settings.json');
10
10
  /**
@@ -136,6 +136,11 @@ export async function launchClaude(options) {
136
136
  args = yolo ? ['--dangerously-skip-permissions', initialPrompt] : [initialPrompt];
137
137
  console.log(pc.dim(`Launching Claude in ${cwd}...`));
138
138
  }
139
+ // Set terminal title and bottom status bar
140
+ const taskLabel = `#${shortIdRaw(taskId)}: ${taskTitle}`;
141
+ setTerminalTitle(taskLabel);
142
+ const statusBar = new TerminalStatusBar(taskLabel);
143
+ statusBar.show();
139
144
  // Launch Claude Code
140
145
  // Use spawn with stdio: 'inherit' for proper TTY passthrough
141
146
  // Signals (Ctrl+C, Escape) are handled naturally since child inherits the terminal
@@ -158,6 +163,9 @@ export async function launchClaude(options) {
158
163
  });
159
164
  child.on('close', () => resolve());
160
165
  });
166
+ // Clean up status bar and terminal title
167
+ statusBar.hide();
168
+ clearTerminalTitle();
161
169
  console.log(pc.dim('\n─────────────────────────────────────────\n'));
162
170
  return { cwd, taskId, apiKey };
163
171
  }
@@ -539,6 +547,10 @@ export async function launchClaudeForReview(options) {
539
547
  '',
540
548
  'IMPORTANT: Do NOT make any code changes. This is a review-only session.',
541
549
  ].join('\n');
550
+ const reviewLabel = `Review #${shortIdRaw(taskId)}`;
551
+ setTerminalTitle(reviewLabel);
552
+ const statusBar = new TerminalStatusBar(reviewLabel);
553
+ statusBar.show();
542
554
  await new Promise((resolve) => {
543
555
  const child = spawn('claude', [prompt], {
544
556
  cwd,
@@ -548,6 +560,8 @@ export async function launchClaudeForReview(options) {
548
560
  child.on('error', () => resolve());
549
561
  child.on('close', () => resolve());
550
562
  });
563
+ statusBar.hide();
564
+ clearTerminalTitle();
551
565
  }
552
566
  /**
553
567
  * Launch Claude to resolve merge conflicts
@@ -557,6 +571,10 @@ export async function launchClaudeForReview(options) {
557
571
  async function launchClaudeForMerge(options) {
558
572
  const { cwd, apiKey } = options;
559
573
  const prompt = 'IMPORTANT: Your ONLY job is to resolve merge conflicts. Do NOT read TASK_CONTEXT.md or work on any task. Run: git merge origin/main --no-edit. If there are conflicts, resolve them, stage, and commit. Do not use any MCP tools.';
574
+ const mergeLabel = 'Resolving merge conflicts';
575
+ setTerminalTitle(mergeLabel);
576
+ const statusBar = new TerminalStatusBar(mergeLabel);
577
+ statusBar.show();
560
578
  await new Promise((resolve) => {
561
579
  const child = spawn('claude', ['--allowedTools', 'Bash,Read,Write,Edit,Glob,Grep', prompt], {
562
580
  cwd,
@@ -566,6 +584,8 @@ async function launchClaudeForMerge(options) {
566
584
  child.on('error', () => resolve());
567
585
  child.on('close', () => resolve());
568
586
  });
587
+ statusBar.hide();
588
+ clearTerminalTitle();
569
589
  }
570
590
  /**
571
591
  * Check if Claude Code CLI is installed
@@ -219,7 +219,7 @@ export declare class DamperApi {
219
219
  title: string;
220
220
  }>;
221
221
  }>;
222
- createTask(title: string, type?: 'bug' | 'feature' | 'improvement' | 'task', description?: string): Promise<Task>;
222
+ createTask(title: string, type?: 'bug' | 'feature' | 'improvement' | 'task', description?: string, isPublic?: boolean): Promise<Task>;
223
223
  deleteTask(taskId: string): Promise<{
224
224
  id: string;
225
225
  deleted: boolean;
@@ -189,8 +189,8 @@ This project uses Damper MCP for task tracking. **You MUST follow this workflow.
189
189
  return this.request('POST', `/api/agent/changelogs/${changelogId}/items`, { taskIds });
190
190
  }
191
191
  // Create task
192
- async createTask(title, type = 'task', description) {
193
- return this.request('POST', '/api/agent/tasks', { title, type, status: 'planned', description });
192
+ async createTask(title, type = 'task', description, isPublic) {
193
+ return this.request('POST', '/api/agent/tasks', { title, type, status: 'planned', description, isPublic });
194
194
  }
195
195
  // Delete task (only planned, no commits)
196
196
  async deleteTask(taskId) {
@@ -27,7 +27,7 @@ ${planSection}
27
27
  1. **Do NOT commit or complete tasks without explicit user confirmation** - Always ask the user before running \`git commit\` or calling \`complete_task\`
28
28
  2. Use \`add_commit\` after each git commit
29
29
  3. Use \`add_note\` ONLY for non-obvious approach decisions (e.g. "Decision: chose X because Y")
30
- 4. When user confirms: call \`complete_task\` with a one-line summary
30
+ 4. When user confirms: call \`complete_task\` with summary and \`reviewInstructions\` (what to test/verify)
31
31
  5. If stopping early: call \`abandon_task\` with what remains and blockers
32
32
 
33
33
  The CLI just bootstrapped this environment - YOU handle the task lifecycle.
@@ -35,6 +35,23 @@ export declare function formatSubtaskProgress(progress: {
35
35
  export declare function relativeTime(date: string | Date | null | undefined): string;
36
36
  /** Returns a picocolors function for the given status */
37
37
  export declare function statusColor(status: string): (s: string) => string;
38
+ /** Set terminal tab/window title via ANSI escape sequence (OSC 0) */
39
+ export declare function setTerminalTitle(title: string): void;
40
+ /** Clear terminal title (restore default behavior) */
41
+ export declare function clearTerminalTitle(): void;
42
+ /**
43
+ * Persistent bottom status bar using ANSI scroll region (DECSTBM).
44
+ * Reserves the last terminal row for a fixed label while the child process
45
+ * operates inside the scroll region above it.
46
+ */
47
+ export declare class TerminalStatusBar {
48
+ private title;
49
+ private handleResize;
50
+ constructor(title: string);
51
+ show(): void;
52
+ hide(): void;
53
+ private draw;
54
+ }
38
55
  /** Terminal width (min 80) */
39
56
  export declare function getTerminalWidth(): number;
40
57
  /** Section header spanning terminal width: `── Label ───────────` */
package/dist/ui/format.js CHANGED
@@ -144,6 +144,62 @@ export function statusColor(status) {
144
144
  default: return pc.dim;
145
145
  }
146
146
  }
147
+ /** Set terminal tab/window title via ANSI escape sequence (OSC 0) */
148
+ export function setTerminalTitle(title) {
149
+ if (process.stdout.isTTY) {
150
+ process.stdout.write(`\x1b]0;${title}\x07`);
151
+ }
152
+ }
153
+ /** Clear terminal title (restore default behavior) */
154
+ export function clearTerminalTitle() {
155
+ if (process.stdout.isTTY) {
156
+ process.stdout.write(`\x1b]0;\x07`);
157
+ }
158
+ }
159
+ /**
160
+ * Persistent bottom status bar using ANSI scroll region (DECSTBM).
161
+ * Reserves the last terminal row for a fixed label while the child process
162
+ * operates inside the scroll region above it.
163
+ */
164
+ export class TerminalStatusBar {
165
+ title;
166
+ handleResize = null;
167
+ constructor(title) {
168
+ this.title = title;
169
+ }
170
+ show() {
171
+ if (!process.stdout.isTTY)
172
+ return;
173
+ this.draw();
174
+ this.handleResize = () => this.draw();
175
+ process.stdout.on('resize', this.handleResize);
176
+ }
177
+ hide() {
178
+ if (!process.stdout.isTTY)
179
+ return;
180
+ if (this.handleResize) {
181
+ process.stdout.off('resize', this.handleResize);
182
+ this.handleResize = null;
183
+ }
184
+ const rows = process.stdout.rows || 24;
185
+ process.stdout.write('\x1b[r' + // Reset scroll region to full terminal
186
+ `\x1b[${rows};1H` + // Move to the status bar row
187
+ '\x1b[2K' // Clear the line
188
+ );
189
+ }
190
+ draw() {
191
+ const rows = process.stdout.rows || 24;
192
+ const cols = process.stdout.columns || 80;
193
+ const bar = ` ${this.title}`.padEnd(cols).slice(0, cols);
194
+ process.stdout.write('\x1b7' + // Save cursor position
195
+ `\x1b[${rows};1H` + // Move to last row
196
+ '\x1b[2K' + // Clear the line
197
+ '\x1b[7m' + bar + '\x1b[0m' + // Draw bar in inverse video
198
+ `\x1b[1;${rows - 1}r` + // Set scroll region above bar
199
+ '\x1b8' // Restore cursor position
200
+ );
201
+ }
202
+ }
147
203
  /** Terminal width (min 80) */
148
204
  export function getTerminalWidth() {
149
205
  return Math.max(80, process.stdout.columns || 80);
@@ -174,11 +174,19 @@ export async function pickTask(options) {
174
174
  }
175
175
  }
176
176
  console.log(pc.bold(`\nProject: ${project}`));
177
+ // Track last search term for passing to create flow
178
+ let lastSearchTerm = '';
177
179
  // Build choices with search filtering support
178
180
  const buildChoices = (term) => {
179
181
  const lowerTerm = term?.toLowerCase().trim() || '';
182
+ lastSearchTerm = term?.trim() || '';
180
183
  const matches = (task) => !lowerTerm || task.title.toLowerCase().includes(lowerTerm) || task.id.includes(lowerTerm);
181
184
  const filtered = [];
185
+ // Always show "Create new task" at the top
186
+ const createLabel = lastSearchTerm
187
+ ? pc.green(`+ New task: "${lastSearchTerm}"`)
188
+ : pc.green('+ Create new task');
189
+ filtered.push({ name: createLabel, value: { type: 'create_new' } });
182
190
  const filteredInProgress = inProgressChoices.filter(c => matches(c.task));
183
191
  if (filteredInProgress.length > 0) {
184
192
  filtered.push(new Separator(`\n${sectionHeader(`In Progress (${filteredInProgress.length})`)}`));
@@ -200,9 +208,6 @@ export async function pickTask(options) {
200
208
  filtered.push({ name: formatTaskChoice(choice, titleWidth, layout), value: choice });
201
209
  }
202
210
  }
203
- // Always show "Create new task"
204
- filtered.push(new Separator(''));
205
- filtered.push({ name: pc.green('+ Create new task'), value: { type: 'create_new' } });
206
211
  return filtered;
207
212
  };
208
213
  const selected = await search({
@@ -211,7 +216,7 @@ export async function pickTask(options) {
211
216
  pageSize: 20,
212
217
  });
213
218
  if (selected.type === 'create_new') {
214
- return handleCreateNewTask(api);
219
+ return handleCreateNewTask(api, lastSearchTerm);
215
220
  }
216
221
  if (selected.type === 'in_progress') {
217
222
  const action = await select({
@@ -260,41 +265,14 @@ export async function pickTask(options) {
260
265
  action,
261
266
  };
262
267
  }
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
- }
291
- async function handleCreateNewTask(api) {
292
- const instructions = await input({
293
- message: 'What needs to be done?',
294
- validate: (value) => value.trim().length > 0 || 'Instructions are required',
268
+ async function handleCreateNewTask(api, searchTerm) {
269
+ const title = await input({
270
+ message: 'Task title:',
271
+ default: searchTerm || undefined,
272
+ validate: (value) => value.trim().length > 0 || 'Title is required',
295
273
  });
296
- const details = await input({
297
- message: 'Additional details (optional, press Enter to skip):',
274
+ const description = await input({
275
+ message: 'Description (optional, press Enter to skip):',
298
276
  });
299
277
  const type = await select({
300
278
  message: 'Task type:',
@@ -305,24 +283,17 @@ async function handleCreateNewTask(api) {
305
283
  { name: 'Task', value: 'task' },
306
284
  ],
307
285
  });
308
- const trimmed = instructions.trim();
309
- const trimmedDetails = details.trim();
310
- const fullInput = trimmedDetails ? `${trimmed}\n\n${trimmedDetails}` : trimmed;
311
- // Generate title with Claude, fall back to truncation
312
- console.log(pc.dim('\nGenerating title...'));
313
- const generatedTitle = await generateTaskTitle(fullInput, type);
314
- let title;
315
- if (generatedTitle) {
316
- title = generatedTitle;
317
- }
318
- else {
319
- const maxTitleLen = 60;
320
- title = trimmed.length <= maxTitleLen
321
- ? trimmed
322
- : trimmed.slice(0, trimmed.lastIndexOf(' ', maxTitleLen) || maxTitleLen) + '…';
323
- }
286
+ const isPublic = await select({
287
+ message: 'Visibility:',
288
+ choices: [
289
+ { name: 'Public', value: true },
290
+ { name: 'Private', value: false },
291
+ ],
292
+ });
293
+ const trimmedTitle = title.trim();
294
+ const trimmedDescription = description.trim() || undefined;
324
295
  console.log(pc.dim('Creating task in Damper...'));
325
- const task = await api.createTask(title, type, fullInput);
296
+ const task = await api.createTask(trimmedTitle, type, trimmedDescription, isPublic);
326
297
  console.log(pc.green(`✓ Created task #${shortIdRaw(task.id)}: ${task.title}`));
327
298
  return {
328
299
  task,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@damper/cli",
3
- "version": "0.9.9",
3
+ "version": "0.9.11",
4
4
  "description": "CLI tool for orchestrating Damper task workflows with Claude Code",
5
5
  "author": "Damper <hello@usedamper.com>",
6
6
  "repository": {