@damper/cli 0.4.1 → 0.5.0
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/commands/cleanup.js +25 -1
- package/dist/commands/release.d.ts +1 -0
- package/dist/commands/release.js +95 -0
- package/dist/commands/start.d.ts +1 -0
- package/dist/commands/start.js +5 -3
- package/dist/index.js +11 -2
- package/dist/services/claude.js +21 -8
- package/dist/services/damper-api.d.ts +5 -0
- package/dist/services/damper-api.js +3 -0
- package/dist/ui/task-picker.d.ts +1 -0
- package/dist/ui/task-picker.js +53 -6
- package/package.json +1 -1
package/dist/commands/cleanup.js
CHANGED
|
@@ -72,6 +72,15 @@ export async function cleanupCommand() {
|
|
|
72
72
|
hasUncommittedChanges: uncommitted,
|
|
73
73
|
});
|
|
74
74
|
}
|
|
75
|
+
else if (task.status === 'in_progress') {
|
|
76
|
+
// Task is still in progress - can drop it
|
|
77
|
+
candidates.push({
|
|
78
|
+
worktree: wt,
|
|
79
|
+
task,
|
|
80
|
+
reason: 'in_progress',
|
|
81
|
+
hasUncommittedChanges: uncommitted,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
75
84
|
}
|
|
76
85
|
catch {
|
|
77
86
|
// Could not fetch task - might be deleted
|
|
@@ -92,7 +101,7 @@ export async function cleanupCommand() {
|
|
|
92
101
|
}
|
|
93
102
|
}
|
|
94
103
|
if (candidates.length === 0) {
|
|
95
|
-
console.log(pc.green('\n✓
|
|
104
|
+
console.log(pc.green('\n✓ No worktrees to clean up.\n'));
|
|
96
105
|
return;
|
|
97
106
|
}
|
|
98
107
|
// Show candidates and let user select
|
|
@@ -111,6 +120,9 @@ export async function cleanupCommand() {
|
|
|
111
120
|
case 'abandoned':
|
|
112
121
|
reasonBadge = pc.yellow('[abandoned]');
|
|
113
122
|
break;
|
|
123
|
+
case 'in_progress':
|
|
124
|
+
reasonBadge = pc.cyan('[in progress - will release]');
|
|
125
|
+
break;
|
|
114
126
|
case 'missing':
|
|
115
127
|
reasonBadge = pc.red('[missing]');
|
|
116
128
|
break;
|
|
@@ -161,6 +173,18 @@ export async function cleanupCommand() {
|
|
|
161
173
|
const { worktree } = candidate;
|
|
162
174
|
const worktreeName = worktree.path.split('/').pop() || worktree.path;
|
|
163
175
|
try {
|
|
176
|
+
// Release the task first if it's in progress
|
|
177
|
+
if (candidate.reason === 'in_progress' && api) {
|
|
178
|
+
try {
|
|
179
|
+
await api.abandonTask(worktree.taskId, 'Dropped via CLI cleanup');
|
|
180
|
+
console.log(pc.green(`✓ Released task #${worktree.taskId}`));
|
|
181
|
+
}
|
|
182
|
+
catch (err) {
|
|
183
|
+
const error = err;
|
|
184
|
+
console.log(pc.yellow(`⚠ Could not release task: ${error.message}`));
|
|
185
|
+
// Continue with worktree removal anyway
|
|
186
|
+
}
|
|
187
|
+
}
|
|
164
188
|
if (candidate.reason === 'missing') {
|
|
165
189
|
// Just remove from state
|
|
166
190
|
removeWorktree(worktree.taskId);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function releaseCommand(): Promise<void>;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { confirm, select } from '@inquirer/prompts';
|
|
2
|
+
import pc from 'picocolors';
|
|
3
|
+
import { createDamperApi } from '../services/damper-api.js';
|
|
4
|
+
import { getWorktrees, removeWorktree } from '../services/state.js';
|
|
5
|
+
import { removeWorktreeDir, getGitRoot } from '../services/worktree.js';
|
|
6
|
+
import { getApiKey } from '../services/config.js';
|
|
7
|
+
export async function releaseCommand() {
|
|
8
|
+
// Get project root
|
|
9
|
+
let projectRoot;
|
|
10
|
+
try {
|
|
11
|
+
projectRoot = await getGitRoot(process.cwd());
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
console.log(pc.red('\nError: Not in a git repository.\n'));
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
// Get API key
|
|
18
|
+
const apiKey = getApiKey(projectRoot);
|
|
19
|
+
if (!apiKey) {
|
|
20
|
+
console.log(pc.yellow('\nProject not configured.'));
|
|
21
|
+
console.log(pc.dim(`Run ${pc.cyan('npx @damper/cli setup')} first.\n`));
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
const api = createDamperApi(apiKey);
|
|
25
|
+
// Get tasks that are in_progress (locked)
|
|
26
|
+
const { tasks, project } = await api.listTasks({ status: 'in_progress' });
|
|
27
|
+
if (tasks.length === 0) {
|
|
28
|
+
console.log(pc.yellow('\nNo in-progress tasks to release.\n'));
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
console.log(pc.bold(`\nProject: ${project}`));
|
|
32
|
+
console.log(pc.dim('Select a task to release back to planned status.\n'));
|
|
33
|
+
// Build choices
|
|
34
|
+
const worktrees = getWorktrees();
|
|
35
|
+
const choices = tasks.map(task => {
|
|
36
|
+
const worktree = worktrees.find(w => w.taskId === task.id);
|
|
37
|
+
const lockedInfo = task.lockedBy ? pc.dim(` (locked by ${task.lockedBy})`) : '';
|
|
38
|
+
const worktreeInfo = worktree ? pc.dim(` [has worktree]`) : '';
|
|
39
|
+
return {
|
|
40
|
+
name: `#${task.id} ${task.title}${lockedInfo}${worktreeInfo}`,
|
|
41
|
+
value: { task, worktree },
|
|
42
|
+
};
|
|
43
|
+
});
|
|
44
|
+
const selected = await select({
|
|
45
|
+
message: 'Select task to release:',
|
|
46
|
+
choices,
|
|
47
|
+
});
|
|
48
|
+
const { task, worktree } = selected;
|
|
49
|
+
// Confirm
|
|
50
|
+
console.log();
|
|
51
|
+
console.log(pc.yellow(`This will:`));
|
|
52
|
+
console.log(pc.dim(` • Release the lock on task #${task.id}`));
|
|
53
|
+
console.log(pc.dim(` • Set task status back to "planned"`));
|
|
54
|
+
if (worktree) {
|
|
55
|
+
console.log(pc.dim(` • Optionally remove the worktree`));
|
|
56
|
+
}
|
|
57
|
+
console.log();
|
|
58
|
+
const shouldRelease = await confirm({
|
|
59
|
+
message: 'Release this task?',
|
|
60
|
+
default: true,
|
|
61
|
+
});
|
|
62
|
+
if (!shouldRelease) {
|
|
63
|
+
console.log(pc.dim('\nCancelled.\n'));
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
// Abandon the task in Damper
|
|
67
|
+
try {
|
|
68
|
+
await api.abandonTask(task.id, 'Released via CLI');
|
|
69
|
+
console.log(pc.green(`\n✓ Task #${task.id} released to planned status`));
|
|
70
|
+
}
|
|
71
|
+
catch (err) {
|
|
72
|
+
const error = err;
|
|
73
|
+
console.log(pc.red(`\nFailed to release task: ${error.message}\n`));
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
// Offer to remove worktree if exists
|
|
77
|
+
if (worktree) {
|
|
78
|
+
const shouldRemoveWorktree = await confirm({
|
|
79
|
+
message: 'Remove the worktree and branch?',
|
|
80
|
+
default: false,
|
|
81
|
+
});
|
|
82
|
+
if (shouldRemoveWorktree) {
|
|
83
|
+
try {
|
|
84
|
+
await removeWorktreeDir(worktree.path, worktree.projectRoot);
|
|
85
|
+
console.log(pc.green('✓ Worktree and branch removed'));
|
|
86
|
+
}
|
|
87
|
+
catch (err) {
|
|
88
|
+
const error = err;
|
|
89
|
+
console.log(pc.yellow(`Could not remove worktree: ${error.message}`));
|
|
90
|
+
removeWorktree(task.id); // At least clean up state
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
console.log();
|
|
95
|
+
}
|
package/dist/commands/start.d.ts
CHANGED
package/dist/commands/start.js
CHANGED
|
@@ -57,6 +57,7 @@ export async function startCommand(options) {
|
|
|
57
57
|
let taskTitle;
|
|
58
58
|
let isResume = false;
|
|
59
59
|
let worktreePath;
|
|
60
|
+
let forceTakeover = options.force || false;
|
|
60
61
|
if (options.taskId) {
|
|
61
62
|
// Direct task selection
|
|
62
63
|
taskId = options.taskId;
|
|
@@ -87,6 +88,7 @@ export async function startCommand(options) {
|
|
|
87
88
|
taskId = result.task.id;
|
|
88
89
|
taskTitle = result.task.title;
|
|
89
90
|
isResume = result.isResume;
|
|
91
|
+
forceTakeover = result.forceTakeover || options.force || false;
|
|
90
92
|
if (result.worktree) {
|
|
91
93
|
worktreePath = result.worktree.path;
|
|
92
94
|
}
|
|
@@ -108,14 +110,14 @@ export async function startCommand(options) {
|
|
|
108
110
|
// New task - lock it first
|
|
109
111
|
console.log(pc.dim('\nLocking task in Damper...'));
|
|
110
112
|
try {
|
|
111
|
-
await api.startTask(taskId);
|
|
112
|
-
console.log(pc.green('✓ Task locked'));
|
|
113
|
+
await api.startTask(taskId, forceTakeover);
|
|
114
|
+
console.log(pc.green(forceTakeover ? '✓ Task lock taken over' : '✓ Task locked'));
|
|
113
115
|
}
|
|
114
116
|
catch (err) {
|
|
115
117
|
const error = err;
|
|
116
118
|
if (error.lockInfo) {
|
|
117
119
|
console.log(pc.yellow(`\n⚠️ Task is locked by "${error.lockInfo.lockedBy}" since ${error.lockInfo.lockedAt}`));
|
|
118
|
-
console.log(pc.dim('Use --force to take over the lock
|
|
120
|
+
console.log(pc.dim('Use --force to take over the lock.\n'));
|
|
119
121
|
process.exit(1);
|
|
120
122
|
}
|
|
121
123
|
throw err;
|
package/dist/index.js
CHANGED
|
@@ -4,7 +4,8 @@ import { startCommand } from './commands/start.js';
|
|
|
4
4
|
import { statusCommand } from './commands/status.js';
|
|
5
5
|
import { cleanupCommand } from './commands/cleanup.js';
|
|
6
6
|
import { setupCommand } from './commands/setup.js';
|
|
7
|
-
|
|
7
|
+
import { releaseCommand } from './commands/release.js';
|
|
8
|
+
const VERSION = '0.5.0';
|
|
8
9
|
function showHelp() {
|
|
9
10
|
console.log(`
|
|
10
11
|
${pc.bold('@damper/cli')} - Agent orchestration for Damper tasks
|
|
@@ -13,12 +14,14 @@ ${pc.bold('Usage:')}
|
|
|
13
14
|
npx @damper/cli Start working on a task (interactive picker)
|
|
14
15
|
npx @damper/cli setup Configure Damper MCP and API key
|
|
15
16
|
npx @damper/cli status Show all in-progress worktrees
|
|
16
|
-
npx @damper/cli cleanup Remove worktrees
|
|
17
|
+
npx @damper/cli cleanup Remove worktrees (completed, abandoned, or in-progress)
|
|
18
|
+
npx @damper/cli release Release a task back to planned status
|
|
17
19
|
|
|
18
20
|
${pc.bold('Options:')}
|
|
19
21
|
--task <id> Work on a specific task
|
|
20
22
|
--type <type> Filter by type: bug, feature, improvement, task
|
|
21
23
|
--status <status> Filter by status: planned, in_progress, done, all
|
|
24
|
+
--force Take over a task locked by another agent
|
|
22
25
|
--yolo Skip plan mode, auto-accept edits
|
|
23
26
|
--reconfigure Change API key (use with setup command)
|
|
24
27
|
-h, --help Show this help message
|
|
@@ -84,6 +87,9 @@ function parseArgs(args) {
|
|
|
84
87
|
else if (arg === '--yolo') {
|
|
85
88
|
result.options.yolo = true;
|
|
86
89
|
}
|
|
90
|
+
else if (arg === '--force') {
|
|
91
|
+
result.options.force = true;
|
|
92
|
+
}
|
|
87
93
|
else if (!arg.startsWith('-') && !result.command) {
|
|
88
94
|
result.command = arg;
|
|
89
95
|
}
|
|
@@ -112,6 +118,9 @@ async function main() {
|
|
|
112
118
|
case 'cleanup':
|
|
113
119
|
await cleanupCommand();
|
|
114
120
|
break;
|
|
121
|
+
case 'release':
|
|
122
|
+
await releaseCommand();
|
|
123
|
+
break;
|
|
115
124
|
case undefined:
|
|
116
125
|
case 'start':
|
|
117
126
|
await startCommand(options);
|
package/dist/services/claude.js
CHANGED
|
@@ -130,18 +130,22 @@ Once approved, switch to implementation and start logging your work.
|
|
|
130
130
|
'Self-improvement: If you encounter friction, bugs, or have ideas to improve the CLI/workflow,',
|
|
131
131
|
'use `report_issue` to log them. Your feedback improves tooling for all future tasks.',
|
|
132
132
|
].join('\n');
|
|
133
|
-
//
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
133
|
+
// Write MCP config to a temp file (more reliable than passing JSON on command line)
|
|
134
|
+
const mcpConfigPath = path.join(os.tmpdir(), `damper-mcp-${taskId}.json`);
|
|
135
|
+
const mcpConfig = {
|
|
136
|
+
mcpServers: {
|
|
137
|
+
damper: {
|
|
138
|
+
command: 'npx',
|
|
139
|
+
args: ['-y', '@damper/mcp'],
|
|
140
|
+
env: { DAMPER_API_KEY: apiKey },
|
|
141
|
+
},
|
|
139
142
|
},
|
|
140
|
-
}
|
|
143
|
+
};
|
|
144
|
+
fs.writeFileSync(mcpConfigPath, JSON.stringify(mcpConfig, null, 2));
|
|
141
145
|
// Build Claude args
|
|
142
146
|
const claudeArgs = [
|
|
143
147
|
'--permission-mode', yolo ? 'acceptEdits' : 'plan',
|
|
144
|
-
'--mcp-config',
|
|
148
|
+
'--mcp-config', mcpConfigPath,
|
|
145
149
|
initialPrompt,
|
|
146
150
|
];
|
|
147
151
|
// Launch Claude Code in interactive mode
|
|
@@ -164,6 +168,15 @@ Once approved, switch to implementation and start logging your work.
|
|
|
164
168
|
}
|
|
165
169
|
// Claude exited with error - still continue to post-task flow
|
|
166
170
|
}
|
|
171
|
+
finally {
|
|
172
|
+
// Clean up temp MCP config file
|
|
173
|
+
try {
|
|
174
|
+
fs.unlinkSync(mcpConfigPath);
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
// Ignore cleanup errors
|
|
178
|
+
}
|
|
179
|
+
}
|
|
167
180
|
// Post-task flow
|
|
168
181
|
console.log(pc.dim('\n─────────────────────────────────────────'));
|
|
169
182
|
console.log(pc.bold('\nClaude session ended.\n'));
|
|
@@ -110,6 +110,11 @@ export declare class DamperApi {
|
|
|
110
110
|
}>;
|
|
111
111
|
getTask(taskId: string): Promise<TaskDetail>;
|
|
112
112
|
startTask(taskId: string, force?: boolean): Promise<StartTaskResult>;
|
|
113
|
+
abandonTask(taskId: string, summary?: string): Promise<{
|
|
114
|
+
id: string;
|
|
115
|
+
status: string;
|
|
116
|
+
message: string;
|
|
117
|
+
}>;
|
|
113
118
|
getProjectContext(taskId?: string): Promise<ContextIndex>;
|
|
114
119
|
getContextSection(section: string): Promise<ContextSection | {
|
|
115
120
|
pattern: string;
|
|
@@ -50,6 +50,9 @@ export class DamperApi {
|
|
|
50
50
|
async startTask(taskId, force) {
|
|
51
51
|
return this.request('POST', `/api/agent/tasks/${taskId}/start`, force ? { force: true } : undefined);
|
|
52
52
|
}
|
|
53
|
+
async abandonTask(taskId, summary) {
|
|
54
|
+
return this.request('POST', `/api/agent/tasks/${taskId}/abandon`, summary ? { summary } : undefined);
|
|
55
|
+
}
|
|
53
56
|
// Project Context
|
|
54
57
|
async getProjectContext(taskId) {
|
|
55
58
|
const params = taskId ? `?task_id=${taskId}` : '';
|
package/dist/ui/task-picker.d.ts
CHANGED
package/dist/ui/task-picker.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { select, Separator } from '@inquirer/prompts';
|
|
1
|
+
import { select, confirm, Separator } from '@inquirer/prompts';
|
|
2
2
|
import pc from 'picocolors';
|
|
3
3
|
function getTypeIcon(type) {
|
|
4
4
|
switch (type) {
|
|
@@ -30,6 +30,11 @@ function formatTaskChoice(choice) {
|
|
|
30
30
|
}
|
|
31
31
|
return description;
|
|
32
32
|
}
|
|
33
|
+
if (choice.type === 'locked') {
|
|
34
|
+
let description = `${pc.dim('#')}${pc.cyan(task.id)} ${task.title} ${pc.dim(`[${typeIcon}]`)} ${priorityIcon}`;
|
|
35
|
+
description += pc.yellow(` [locked by ${choice.lockedBy}]`);
|
|
36
|
+
return description;
|
|
37
|
+
}
|
|
33
38
|
// Available task
|
|
34
39
|
let description = `${pc.dim('#')}${pc.cyan(task.id)} ${task.title} ${pc.dim(`[${typeIcon}]`)} ${priorityIcon}`;
|
|
35
40
|
if (task.quarter) {
|
|
@@ -70,11 +75,26 @@ export async function pickTask(options) {
|
|
|
70
75
|
});
|
|
71
76
|
}
|
|
72
77
|
}
|
|
73
|
-
//
|
|
74
|
-
const availableChoices =
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
+
// Separate available tasks from locked tasks
|
|
79
|
+
const availableChoices = [];
|
|
80
|
+
const lockedChoices = [];
|
|
81
|
+
for (const task of availableTasks) {
|
|
82
|
+
if (worktreeTaskIds.has(task.id))
|
|
83
|
+
continue;
|
|
84
|
+
const taskAny = task;
|
|
85
|
+
if (task.status === 'in_progress' && taskAny.lockedBy) {
|
|
86
|
+
// Task is locked by someone else
|
|
87
|
+
lockedChoices.push({
|
|
88
|
+
type: 'locked',
|
|
89
|
+
task,
|
|
90
|
+
lockedBy: taskAny.lockedBy,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
else if (task.status === 'planned') {
|
|
94
|
+
availableChoices.push({ type: 'available', task });
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
if (inProgressChoices.length === 0 && availableChoices.length === 0 && lockedChoices.length === 0) {
|
|
78
98
|
console.log(pc.yellow('\nNo tasks available.'));
|
|
79
99
|
if (typeFilter) {
|
|
80
100
|
console.log(pc.dim(`(filtered by type: ${typeFilter})`));
|
|
@@ -101,6 +121,15 @@ export async function pickTask(options) {
|
|
|
101
121
|
});
|
|
102
122
|
}
|
|
103
123
|
}
|
|
124
|
+
if (lockedChoices.length > 0) {
|
|
125
|
+
choices.push(new Separator(pc.bold('\n--- Locked Tasks (can take over) ---')));
|
|
126
|
+
for (const choice of lockedChoices) {
|
|
127
|
+
choices.push({
|
|
128
|
+
name: formatTaskChoice(choice),
|
|
129
|
+
value: choice,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
}
|
|
104
133
|
console.log(pc.bold(`\nProject: ${project}`));
|
|
105
134
|
const selected = await select({
|
|
106
135
|
message: 'Select a task to work on:',
|
|
@@ -114,6 +143,24 @@ export async function pickTask(options) {
|
|
|
114
143
|
isResume: true,
|
|
115
144
|
};
|
|
116
145
|
}
|
|
146
|
+
if (selected.type === 'locked') {
|
|
147
|
+
// Prompt to take over
|
|
148
|
+
console.log();
|
|
149
|
+
console.log(pc.yellow(`This task is locked by "${selected.lockedBy}".`));
|
|
150
|
+
const shouldTakeover = await confirm({
|
|
151
|
+
message: 'Take over this task?',
|
|
152
|
+
default: true,
|
|
153
|
+
});
|
|
154
|
+
if (!shouldTakeover) {
|
|
155
|
+
console.log(pc.dim('\nCancelled.\n'));
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
return {
|
|
159
|
+
task: selected.task,
|
|
160
|
+
isResume: false,
|
|
161
|
+
forceTakeover: true,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
117
164
|
return {
|
|
118
165
|
task: selected.task,
|
|
119
166
|
isResume: false,
|