@damper/cli 0.9.16 → 0.9.18

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.
@@ -5,7 +5,7 @@ import { createWorktree, getMainProjectRoot, removeWorktreeDir } from '../servic
5
5
  import { bootstrapContext, refreshContext } from '../services/context-bootstrap.js';
6
6
  import { pickTask } from '../ui/task-picker.js';
7
7
  import { launchClaude, launchClaudeForReview, postTaskFlow, isClaudeInstalled, isDamperMcpConfigured, configureDamperMcp } from '../services/claude.js';
8
- import { getWorktreesForProject, cleanupStaleWorktrees, getPreference, setPreference } from '../services/state.js';
8
+ import { getWorktreesForProject, cleanupStaleWorktrees } from '../services/state.js';
9
9
  import { getApiKey, isProjectConfigured, getProjectConfigPath } from '../services/config.js';
10
10
  import { shortIdRaw } from '../ui/format.js';
11
11
  export async function startCommand(options) {
@@ -39,8 +39,6 @@ export async function startCommand(options) {
39
39
  }
40
40
  process.exit(1);
41
41
  }
42
- // Check if tmux is installed (enables persistent task title bar)
43
- await ensureTmux();
44
42
  // Ensure MCP is configured globally (without key - key comes from env)
45
43
  if (!isDamperMcpConfigured()) {
46
44
  console.log(pc.dim('Configuring Damper MCP...'));
@@ -312,77 +310,3 @@ async function handleReviewAndComplete(options) {
312
310
  }
313
311
  console.log();
314
312
  }
315
- /**
316
- * Check if tmux is installed; offer to install or recommend it if not.
317
- * Skipped if the user previously dismissed the prompt.
318
- */
319
- async function ensureTmux() {
320
- // Already dismissed?
321
- if (getPreference('tmuxPromptDismissed'))
322
- return;
323
- // Already installed?
324
- try {
325
- const { execa } = await import('execa');
326
- await execa('tmux', ['-V'], { stdio: 'pipe' });
327
- return; // tmux is available
328
- }
329
- catch {
330
- // not installed — continue
331
- }
332
- // Check if brew is available
333
- let hasBrew = false;
334
- try {
335
- const { execa } = await import('execa');
336
- await execa('brew', ['--version'], { stdio: 'pipe' });
337
- hasBrew = true;
338
- }
339
- catch {
340
- // no brew
341
- }
342
- console.log(pc.yellow('\n⚠ tmux not found'));
343
- console.log(pc.dim(' tmux enables a persistent task title bar while Claude runs.'));
344
- if (hasBrew) {
345
- const { select } = await import('@inquirer/prompts');
346
- const action = await select({
347
- message: 'Install tmux via Homebrew?',
348
- choices: [
349
- { name: 'Install now (brew install tmux)', value: 'install' },
350
- { name: 'Skip for now', value: 'skip' },
351
- { name: "Don't ask again", value: 'dismiss' },
352
- ],
353
- });
354
- if (action === 'install') {
355
- console.log(pc.dim('\nInstalling tmux...'));
356
- try {
357
- const { execa } = await import('execa');
358
- await execa('brew', ['install', 'tmux'], { stdio: 'inherit' });
359
- console.log(pc.green('✓ tmux installed\n'));
360
- }
361
- catch {
362
- console.log(pc.red('Failed to install tmux. You can install it manually: brew install tmux\n'));
363
- }
364
- }
365
- else if (action === 'dismiss') {
366
- setPreference('tmuxPromptDismissed', true);
367
- console.log(pc.dim('You can install it later: brew install tmux\n'));
368
- }
369
- else {
370
- console.log(pc.dim('Continuing without tmux.\n'));
371
- }
372
- }
373
- else {
374
- console.log(pc.dim(' Install it with: brew install tmux'));
375
- console.log(pc.dim(' Or see: https://github.com/tmux/tmux/wiki/Installing\n'));
376
- const { select } = await import('@inquirer/prompts');
377
- const action = await select({
378
- message: 'What would you like to do?',
379
- choices: [
380
- { name: 'Continue without tmux', value: 'skip' },
381
- { name: "Don't remind me again", value: 'dismiss' },
382
- ],
383
- });
384
- if (action === 'dismiss') {
385
- setPreference('tmuxPromptDismissed', true);
386
- }
387
- }
388
- }
@@ -92,8 +92,8 @@ function notifySessionEnd(label) {
92
92
  process.stdout.write('\x07');
93
93
  }
94
94
  if (process.platform === 'darwin') {
95
- // Play sound directly (reliable, doesn't need notification permissions)
96
- spawn('afplay', ['/System/Library/Sounds/Glass.aiff'], {
95
+ // Play system alert sound (uses whatever is set in System Settings > Sound)
96
+ spawn('osascript', ['-e', 'beep'], {
97
97
  stdio: 'ignore', detached: true,
98
98
  }).unref();
99
99
  // Desktop notification banner (needs notification permissions for Script Editor)
@@ -104,62 +104,6 @@ function notifySessionEnd(label) {
104
104
  ], { stdio: 'ignore', detached: true }).unref();
105
105
  }
106
106
  }
107
- /**
108
- * Check if tmux is available
109
- */
110
- async function isTmuxAvailable() {
111
- try {
112
- await execa('tmux', ['-V'], { stdio: 'pipe' });
113
- return true;
114
- }
115
- catch {
116
- return false;
117
- }
118
- }
119
- /**
120
- * Launch a command inside a tmux session with a persistent status bar.
121
- * The session auto-exits when the command finishes.
122
- */
123
- async function launchInTmux(options) {
124
- const { label, command, args, cwd, env } = options;
125
- const sessionName = `damper-${Date.now()}`;
126
- // Build the full command string to run inside tmux
127
- // When the command finishes, tmux exits automatically via remain-on-exit off
128
- const escapedArgs = args.map(a => `'${a.replace(/'/g, "'\\''")}'`).join(' ');
129
- const fullCmd = `${command} ${escapedArgs}`;
130
- // Create a detached tmux session running the command directly
131
- // Using tmux new-session with the command means the session dies when the command exits
132
- await execa('tmux', [
133
- 'new-session', '-d',
134
- '-s', sessionName,
135
- '-x', String(process.stdout.columns || 80),
136
- '-y', String(process.stdout.rows || 24),
137
- fullCmd,
138
- ], { cwd, env: { ...process.env, ...env }, stdio: 'pipe' });
139
- // Configure the status bar
140
- const statusBarCmds = [
141
- ['set-option', '-t', sessionName, 'status', 'on'],
142
- ['set-option', '-t', sessionName, 'status-position', 'top'],
143
- ['set-option', '-t', sessionName, 'status-style', 'bg=#1e1e2e,fg=#cdd6f4'],
144
- ['set-option', '-t', sessionName, 'status-left', ` ${label} `],
145
- ['set-option', '-t', sessionName, 'status-left-length', '100'],
146
- ['set-option', '-t', sessionName, 'status-right', ''],
147
- ];
148
- for (const cmd of statusBarCmds) {
149
- await execa('tmux', cmd, { stdio: 'pipe' }).catch(() => { });
150
- }
151
- // Attach to the session — blocks until Claude exits and tmux session ends
152
- await new Promise((resolve) => {
153
- const child = spawn('tmux', ['attach-session', '-t', sessionName], {
154
- cwd,
155
- stdio: 'inherit',
156
- });
157
- child.on('error', () => resolve());
158
- child.on('close', () => resolve());
159
- });
160
- // Cleanup in case session lingers
161
- await execa('tmux', ['kill-session', '-t', sessionName], { stdio: 'pipe' }).catch(() => { });
162
- }
163
107
  /**
164
108
  * Launch Claude Code in a directory
165
109
  */
@@ -215,42 +159,29 @@ export async function launchClaude(options) {
215
159
  args = yolo ? ['--dangerously-skip-permissions', initialPrompt] : [initialPrompt];
216
160
  console.log(pc.dim(`Launching Claude in ${cwd}...`));
217
161
  }
218
- // Set terminal title (works in tab bar regardless of tmux)
162
+ // Set terminal title so task is visible in tab bar
219
163
  const taskLabel = `#${shortIdRaw(taskId)}: ${taskTitle}`;
220
164
  setTerminalTitle(taskLabel);
221
- const hasTmux = await isTmuxAvailable();
222
- if (hasTmux) {
223
- // Launch inside tmux with a persistent status bar
224
- await launchInTmux({
225
- label: taskLabel,
226
- command: 'claude',
227
- args,
165
+ // Launch Claude Code with stdio: 'inherit' for proper TTY passthrough
166
+ await new Promise((resolve) => {
167
+ const child = spawn('claude', args, {
228
168
  cwd,
229
- env: { DAMPER_API_KEY: apiKey },
169
+ stdio: 'inherit',
170
+ env: {
171
+ ...process.env,
172
+ DAMPER_API_KEY: apiKey,
173
+ },
230
174
  });
231
- }
232
- else {
233
- // Fallback: direct spawn (no status bar, just terminal title)
234
- await new Promise((resolve) => {
235
- const child = spawn('claude', args, {
236
- cwd,
237
- stdio: 'inherit',
238
- env: {
239
- ...process.env,
240
- DAMPER_API_KEY: apiKey,
241
- },
242
- });
243
- child.on('error', (err) => {
244
- if (err.code === 'ENOENT') {
245
- console.log(pc.red('\nError: Claude Code CLI not found.'));
246
- console.log(pc.dim('Install it with: npm install -g @anthropic-ai/claude-code\n'));
247
- process.exit(1);
248
- }
249
- resolve();
250
- });
251
- child.on('close', () => resolve());
175
+ child.on('error', (err) => {
176
+ if (err.code === 'ENOENT') {
177
+ console.log(pc.red('\nError: Claude Code CLI not found.'));
178
+ console.log(pc.dim('Install it with: npm install -g @anthropic-ai/claude-code\n'));
179
+ process.exit(1);
180
+ }
181
+ resolve();
252
182
  });
253
- }
183
+ child.on('close', () => resolve());
184
+ });
254
185
  clearTerminalTitle();
255
186
  notifySessionEnd(`Claude finished: ${taskLabel}`);
256
187
  console.log(pc.dim('\n─────────────────────────────────────────\n'));
@@ -636,27 +567,15 @@ export async function launchClaudeForReview(options) {
636
567
  ].join('\n');
637
568
  const reviewLabel = `Review #${shortIdRaw(taskId)}`;
638
569
  setTerminalTitle(reviewLabel);
639
- const hasTmux = await isTmuxAvailable();
640
- if (hasTmux) {
641
- await launchInTmux({
642
- label: reviewLabel,
643
- command: 'claude',
644
- args: [prompt],
570
+ await new Promise((resolve) => {
571
+ const child = spawn('claude', [prompt], {
645
572
  cwd,
646
- env: { DAMPER_API_KEY: apiKey },
647
- });
648
- }
649
- else {
650
- await new Promise((resolve) => {
651
- const child = spawn('claude', [prompt], {
652
- cwd,
653
- stdio: 'inherit',
654
- env: { ...process.env, DAMPER_API_KEY: apiKey },
655
- });
656
- child.on('error', () => resolve());
657
- child.on('close', () => resolve());
573
+ stdio: 'inherit',
574
+ env: { ...process.env, DAMPER_API_KEY: apiKey },
658
575
  });
659
- }
576
+ child.on('error', () => resolve());
577
+ child.on('close', () => resolve());
578
+ });
660
579
  clearTerminalTitle();
661
580
  notifySessionEnd(`Review finished: ${reviewLabel}`);
662
581
  }
@@ -670,27 +589,15 @@ async function launchClaudeForMerge(options) {
670
589
  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.';
671
590
  const mergeLabel = 'Resolving merge conflicts';
672
591
  setTerminalTitle(mergeLabel);
673
- const hasTmux = await isTmuxAvailable();
674
- if (hasTmux) {
675
- await launchInTmux({
676
- label: mergeLabel,
677
- command: 'claude',
678
- args: ['--allowedTools', 'Bash,Read,Write,Edit,Glob,Grep', prompt],
592
+ await new Promise((resolve) => {
593
+ const child = spawn('claude', ['--allowedTools', 'Bash,Read,Write,Edit,Glob,Grep', prompt], {
679
594
  cwd,
680
- env: { DAMPER_API_KEY: apiKey },
681
- });
682
- }
683
- else {
684
- await new Promise((resolve) => {
685
- const child = spawn('claude', ['--allowedTools', 'Bash,Read,Write,Edit,Glob,Grep', prompt], {
686
- cwd,
687
- stdio: 'inherit',
688
- env: { ...process.env, DAMPER_API_KEY: apiKey },
689
- });
690
- child.on('error', () => resolve());
691
- child.on('close', () => resolve());
595
+ stdio: 'inherit',
596
+ env: { ...process.env, DAMPER_API_KEY: apiKey },
692
597
  });
693
- }
598
+ child.on('error', () => resolve());
599
+ child.on('close', () => resolve());
600
+ });
694
601
  clearTerminalTitle();
695
602
  notifySessionEnd('Merge conflict resolution finished');
696
603
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@damper/cli",
3
- "version": "0.9.16",
3
+ "version": "0.9.18",
4
4
  "description": "CLI tool for orchestrating Damper task workflows with Claude Code",
5
5
  "author": "Damper <hello@usedamper.com>",
6
6
  "repository": {