@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.
- package/dist/services/claude.js +21 -1
- package/dist/services/damper-api.d.ts +1 -1
- package/dist/services/damper-api.js +2 -2
- package/dist/templates/CLAUDE_APPEND.md.js +1 -1
- package/dist/ui/format.d.ts +17 -0
- package/dist/ui/format.js +56 -0
- package/dist/ui/task-picker.js +26 -55
- package/package.json +1 -1
package/dist/services/claude.js
CHANGED
|
@@ -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
|
|
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.
|
package/dist/ui/format.d.ts
CHANGED
|
@@ -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);
|
package/dist/ui/task-picker.js
CHANGED
|
@@ -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
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
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
|
|
297
|
-
message: '
|
|
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
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
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(
|
|
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,
|