@damper/cli 0.1.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.d.ts +1 -0
- package/dist/commands/cleanup.js +149 -0
- package/dist/commands/setup.d.ts +1 -0
- package/dist/commands/setup.js +67 -0
- package/dist/commands/start.d.ts +6 -0
- package/dist/commands/start.js +139 -0
- package/dist/commands/status.d.ts +1 -0
- package/dist/commands/status.js +74 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +125 -0
- package/dist/services/claude.d.ts +44 -0
- package/dist/services/claude.js +221 -0
- package/dist/services/context-bootstrap.d.ts +18 -0
- package/dist/services/context-bootstrap.js +94 -0
- package/dist/services/damper-api.d.ts +154 -0
- package/dist/services/damper-api.js +151 -0
- package/dist/services/state.d.ts +15 -0
- package/dist/services/state.js +77 -0
- package/dist/services/worktree.d.ts +34 -0
- package/dist/services/worktree.js +289 -0
- package/dist/templates/CLAUDE_APPEND.md.d.ts +6 -0
- package/dist/templates/CLAUDE_APPEND.md.js +17 -0
- package/dist/templates/TASK_CONTEXT.md.d.ts +15 -0
- package/dist/templates/TASK_CONTEXT.md.js +115 -0
- package/dist/ui/task-picker.d.ts +15 -0
- package/dist/ui/task-picker.js +121 -0
- package/dist/utils/config.d.ts +7 -0
- package/dist/utils/config.js +32 -0
- package/package.json +50 -0
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import * as os from 'node:os';
|
|
4
|
+
import { execa } from 'execa';
|
|
5
|
+
import { confirm, password } from '@inquirer/prompts';
|
|
6
|
+
import pc from 'picocolors';
|
|
7
|
+
const CLAUDE_SETTINGS_DIR = path.join(os.homedir(), '.claude');
|
|
8
|
+
const CLAUDE_SETTINGS_FILE = path.join(CLAUDE_SETTINGS_DIR, 'settings.json');
|
|
9
|
+
/**
|
|
10
|
+
* Check if Damper MCP is configured in Claude settings
|
|
11
|
+
*/
|
|
12
|
+
export function isDamperMcpConfigured() {
|
|
13
|
+
if (!fs.existsSync(CLAUDE_SETTINGS_FILE)) {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
try {
|
|
17
|
+
const settings = JSON.parse(fs.readFileSync(CLAUDE_SETTINGS_FILE, 'utf-8'));
|
|
18
|
+
return !!settings.mcpServers?.damper;
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Get the current API key from MCP config or environment
|
|
26
|
+
*/
|
|
27
|
+
export function getConfiguredApiKey() {
|
|
28
|
+
// First check environment
|
|
29
|
+
if (process.env.DAMPER_API_KEY) {
|
|
30
|
+
return process.env.DAMPER_API_KEY;
|
|
31
|
+
}
|
|
32
|
+
// Then check Claude settings
|
|
33
|
+
if (!fs.existsSync(CLAUDE_SETTINGS_FILE)) {
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
try {
|
|
37
|
+
const settings = JSON.parse(fs.readFileSync(CLAUDE_SETTINGS_FILE, 'utf-8'));
|
|
38
|
+
return settings.mcpServers?.damper?.env?.DAMPER_API_KEY;
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Get the recommended MCP configuration
|
|
46
|
+
*/
|
|
47
|
+
export function getDamperMcpConfig(apiKey) {
|
|
48
|
+
const config = {
|
|
49
|
+
command: 'npx',
|
|
50
|
+
args: ['-y', '@damper/mcp'],
|
|
51
|
+
};
|
|
52
|
+
if (apiKey) {
|
|
53
|
+
config.env = { DAMPER_API_KEY: apiKey };
|
|
54
|
+
}
|
|
55
|
+
return config;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Configure Damper MCP in Claude settings
|
|
59
|
+
*/
|
|
60
|
+
export function configureDamperMcp(apiKey) {
|
|
61
|
+
// Ensure directory exists
|
|
62
|
+
if (!fs.existsSync(CLAUDE_SETTINGS_DIR)) {
|
|
63
|
+
fs.mkdirSync(CLAUDE_SETTINGS_DIR, { recursive: true });
|
|
64
|
+
}
|
|
65
|
+
// Load existing settings or create new
|
|
66
|
+
let settings = {};
|
|
67
|
+
if (fs.existsSync(CLAUDE_SETTINGS_FILE)) {
|
|
68
|
+
try {
|
|
69
|
+
settings = JSON.parse(fs.readFileSync(CLAUDE_SETTINGS_FILE, 'utf-8'));
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
// Start fresh if corrupted
|
|
73
|
+
settings = {};
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
// Add/update MCP config
|
|
77
|
+
if (!settings.mcpServers) {
|
|
78
|
+
settings.mcpServers = {};
|
|
79
|
+
}
|
|
80
|
+
settings.mcpServers.damper = getDamperMcpConfig(apiKey);
|
|
81
|
+
// Write back
|
|
82
|
+
fs.writeFileSync(CLAUDE_SETTINGS_FILE, JSON.stringify(settings, null, 2));
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Interactive setup for Damper MCP
|
|
86
|
+
* Returns the API key (either existing or newly entered)
|
|
87
|
+
*/
|
|
88
|
+
export async function setupDamperMcp() {
|
|
89
|
+
console.log(pc.yellow('\nDamper MCP is not configured in Claude Code.'));
|
|
90
|
+
console.log(pc.dim('The MCP server allows Claude to interact with your Damper tasks.\n'));
|
|
91
|
+
const shouldSetup = await confirm({
|
|
92
|
+
message: 'Would you like to set it up now?',
|
|
93
|
+
default: true,
|
|
94
|
+
});
|
|
95
|
+
if (!shouldSetup) {
|
|
96
|
+
console.log(pc.dim('\nYou can set it up later by running:'));
|
|
97
|
+
console.log(pc.cyan(' claude mcp add damper npx @damper/mcp\n'));
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
// Check if we already have an API key in the environment
|
|
101
|
+
const existingKey = process.env.DAMPER_API_KEY;
|
|
102
|
+
let apiKey;
|
|
103
|
+
if (existingKey) {
|
|
104
|
+
console.log(pc.dim('\nFound DAMPER_API_KEY in environment.'));
|
|
105
|
+
const useExisting = await confirm({
|
|
106
|
+
message: 'Use the API key from your environment?',
|
|
107
|
+
default: true,
|
|
108
|
+
});
|
|
109
|
+
if (useExisting) {
|
|
110
|
+
apiKey = existingKey;
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
apiKey = await promptForApiKey();
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
apiKey = await promptForApiKey();
|
|
118
|
+
}
|
|
119
|
+
// Configure MCP
|
|
120
|
+
console.log(pc.dim('\nConfiguring Damper MCP...'));
|
|
121
|
+
configureDamperMcp(apiKey);
|
|
122
|
+
console.log(pc.green('✓ Damper MCP configured in ~/.claude/settings.json'));
|
|
123
|
+
// Set in environment for current session
|
|
124
|
+
process.env.DAMPER_API_KEY = apiKey;
|
|
125
|
+
return apiKey;
|
|
126
|
+
}
|
|
127
|
+
async function promptForApiKey() {
|
|
128
|
+
console.log(pc.dim('\nGet your API key from: https://usedamper.com → Settings → API Keys\n'));
|
|
129
|
+
const apiKey = await password({
|
|
130
|
+
message: 'Enter your Damper API key:',
|
|
131
|
+
mask: '*',
|
|
132
|
+
validate: (value) => {
|
|
133
|
+
if (!value || value.trim().length === 0) {
|
|
134
|
+
return 'API key is required';
|
|
135
|
+
}
|
|
136
|
+
if (value.length < 10) {
|
|
137
|
+
return 'API key seems too short';
|
|
138
|
+
}
|
|
139
|
+
return true;
|
|
140
|
+
},
|
|
141
|
+
});
|
|
142
|
+
return apiKey.trim();
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Ensure MCP is configured, offering to set it up if not
|
|
146
|
+
* Returns the API key to use
|
|
147
|
+
*/
|
|
148
|
+
export async function ensureMcpConfigured() {
|
|
149
|
+
// Check if already configured
|
|
150
|
+
if (isDamperMcpConfigured()) {
|
|
151
|
+
const apiKey = getConfiguredApiKey();
|
|
152
|
+
if (apiKey) {
|
|
153
|
+
return apiKey;
|
|
154
|
+
}
|
|
155
|
+
// MCP configured but no API key - need to add it
|
|
156
|
+
console.log(pc.yellow('\nDamper MCP is configured but no API key found.'));
|
|
157
|
+
const key = await promptForApiKey();
|
|
158
|
+
configureDamperMcp(key);
|
|
159
|
+
process.env.DAMPER_API_KEY = key;
|
|
160
|
+
return key;
|
|
161
|
+
}
|
|
162
|
+
// Not configured - run setup
|
|
163
|
+
const apiKey = await setupDamperMcp();
|
|
164
|
+
if (!apiKey) {
|
|
165
|
+
// User declined setup
|
|
166
|
+
console.log(pc.red('\nCannot continue without Damper MCP configuration.'));
|
|
167
|
+
process.exit(1);
|
|
168
|
+
}
|
|
169
|
+
return apiKey;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Launch Claude Code in a directory
|
|
173
|
+
*/
|
|
174
|
+
export async function launchClaude(options) {
|
|
175
|
+
const { cwd, taskId, taskTitle } = options;
|
|
176
|
+
console.log(pc.green(`\nStarting Claude Code for task #${taskId}: ${taskTitle}`));
|
|
177
|
+
console.log(pc.dim(`Directory: ${cwd}\n`));
|
|
178
|
+
// Build initial prompt
|
|
179
|
+
const initialPrompt = [
|
|
180
|
+
`I'm working on task #${taskId}: ${taskTitle}`,
|
|
181
|
+
'',
|
|
182
|
+
'Please read TASK_CONTEXT.md for full context, critical rules, and the implementation plan.',
|
|
183
|
+
'',
|
|
184
|
+
'Remember to:',
|
|
185
|
+
'1. Use `add_commit` after each git commit',
|
|
186
|
+
'2. Use `add_note` for important decisions',
|
|
187
|
+
'3. Call `complete_task` when done or `abandon_task` if stopping early',
|
|
188
|
+
].join('\n');
|
|
189
|
+
// Launch Claude Code
|
|
190
|
+
// Using subprocess with inherited stdio so user can interact
|
|
191
|
+
try {
|
|
192
|
+
await execa('claude', ['--print', initialPrompt], {
|
|
193
|
+
cwd,
|
|
194
|
+
stdio: 'inherit',
|
|
195
|
+
env: {
|
|
196
|
+
...process.env,
|
|
197
|
+
},
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
catch (err) {
|
|
201
|
+
const error = err;
|
|
202
|
+
if (error.code === 'ENOENT') {
|
|
203
|
+
console.log(pc.red('\nError: Claude Code CLI not found.'));
|
|
204
|
+
console.log(pc.dim('Install it with: npm install -g @anthropic-ai/claude-code\n'));
|
|
205
|
+
process.exit(1);
|
|
206
|
+
}
|
|
207
|
+
throw err;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Check if Claude Code CLI is installed
|
|
212
|
+
*/
|
|
213
|
+
export async function isClaudeInstalled() {
|
|
214
|
+
try {
|
|
215
|
+
await execa('claude', ['--version'], { stdio: 'pipe' });
|
|
216
|
+
return true;
|
|
217
|
+
}
|
|
218
|
+
catch {
|
|
219
|
+
return false;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { DamperApi } from './damper-api.js';
|
|
2
|
+
export interface BootstrapOptions {
|
|
3
|
+
api: DamperApi;
|
|
4
|
+
taskId: string;
|
|
5
|
+
worktreePath: string;
|
|
6
|
+
}
|
|
7
|
+
export interface BootstrapResult {
|
|
8
|
+
taskContextPath: string;
|
|
9
|
+
claudeMdUpdated: boolean;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Fetch all relevant context from Damper and write local files
|
|
13
|
+
*/
|
|
14
|
+
export declare function bootstrapContext(options: BootstrapOptions): Promise<BootstrapResult>;
|
|
15
|
+
/**
|
|
16
|
+
* Refresh TASK_CONTEXT.md with latest data from Damper (for resume scenarios)
|
|
17
|
+
*/
|
|
18
|
+
export declare function refreshContext(options: BootstrapOptions): Promise<BootstrapResult>;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import pc from 'picocolors';
|
|
4
|
+
import { generateTaskContext } from '../templates/TASK_CONTEXT.md.js';
|
|
5
|
+
import { generateClaudeAppend } from '../templates/CLAUDE_APPEND.md.js';
|
|
6
|
+
const TASK_CONTEXT_FILE = 'TASK_CONTEXT.md';
|
|
7
|
+
const CLAUDE_MD_FILE = 'CLAUDE.md';
|
|
8
|
+
const TASK_SECTION_MARKER = '## Current Task:';
|
|
9
|
+
/**
|
|
10
|
+
* Fetch all relevant context from Damper and write local files
|
|
11
|
+
*/
|
|
12
|
+
export async function bootstrapContext(options) {
|
|
13
|
+
const { api, taskId, worktreePath } = options;
|
|
14
|
+
console.log(pc.dim('Fetching task details...'));
|
|
15
|
+
const task = await api.getTask(taskId);
|
|
16
|
+
console.log(pc.dim('Fetching project context...'));
|
|
17
|
+
const context = await api.getProjectContext(taskId);
|
|
18
|
+
// Fetch full content of relevant sections
|
|
19
|
+
const sections = [];
|
|
20
|
+
const relevantSections = context.relevantSections || [];
|
|
21
|
+
if (relevantSections.length > 0) {
|
|
22
|
+
console.log(pc.dim(`Fetching ${relevantSections.length} relevant context sections...`));
|
|
23
|
+
for (const sectionName of relevantSections) {
|
|
24
|
+
try {
|
|
25
|
+
const result = await api.getContextSection(sectionName);
|
|
26
|
+
// Handle both single section and glob pattern responses
|
|
27
|
+
if ('pattern' in result && result.sections) {
|
|
28
|
+
sections.push(...result.sections);
|
|
29
|
+
}
|
|
30
|
+
else if ('section' in result && 'content' in result) {
|
|
31
|
+
sections.push(result);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
catch (err) {
|
|
35
|
+
console.log(pc.dim(` Could not fetch section: ${sectionName}`));
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
// Fetch templates
|
|
40
|
+
console.log(pc.dim('Fetching templates...'));
|
|
41
|
+
const templatesResult = await api.listTemplates();
|
|
42
|
+
const templates = templatesResult.isEmpty ? [] : templatesResult.templates;
|
|
43
|
+
// Fetch modules
|
|
44
|
+
console.log(pc.dim('Fetching modules...'));
|
|
45
|
+
const modulesResult = await api.listModules();
|
|
46
|
+
const modules = modulesResult.isEmpty ? [] : modulesResult.modules;
|
|
47
|
+
// Get agent instructions
|
|
48
|
+
console.log(pc.dim('Fetching Damper workflow instructions...'));
|
|
49
|
+
const instructions = await api.getAgentInstructions('section');
|
|
50
|
+
// Generate TASK_CONTEXT.md
|
|
51
|
+
const taskContext = generateTaskContext({
|
|
52
|
+
task,
|
|
53
|
+
criticalRules: context.criticalRules || [],
|
|
54
|
+
sections,
|
|
55
|
+
templates,
|
|
56
|
+
modules,
|
|
57
|
+
damperInstructions: instructions.content,
|
|
58
|
+
});
|
|
59
|
+
// Write TASK_CONTEXT.md
|
|
60
|
+
const taskContextPath = path.join(worktreePath, TASK_CONTEXT_FILE);
|
|
61
|
+
console.log(pc.dim(`Writing ${TASK_CONTEXT_FILE}...`));
|
|
62
|
+
await fs.promises.writeFile(taskContextPath, taskContext, 'utf-8');
|
|
63
|
+
// Update CLAUDE.md with task section
|
|
64
|
+
const claudeMdPath = path.join(worktreePath, CLAUDE_MD_FILE);
|
|
65
|
+
let claudeMdUpdated = false;
|
|
66
|
+
if (fs.existsSync(claudeMdPath)) {
|
|
67
|
+
console.log(pc.dim(`Updating ${CLAUDE_MD_FILE}...`));
|
|
68
|
+
let claudeMd = await fs.promises.readFile(claudeMdPath, 'utf-8');
|
|
69
|
+
// Remove any existing task section
|
|
70
|
+
const markerIndex = claudeMd.indexOf(TASK_SECTION_MARKER);
|
|
71
|
+
if (markerIndex !== -1) {
|
|
72
|
+
claudeMd = claudeMd.slice(0, markerIndex).trimEnd();
|
|
73
|
+
}
|
|
74
|
+
// Append new task section
|
|
75
|
+
const taskSection = generateClaudeAppend({
|
|
76
|
+
taskId,
|
|
77
|
+
taskTitle: task.title,
|
|
78
|
+
});
|
|
79
|
+
claudeMd = `${claudeMd}\n\n${taskSection}\n`;
|
|
80
|
+
await fs.promises.writeFile(claudeMdPath, claudeMd, 'utf-8');
|
|
81
|
+
claudeMdUpdated = true;
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
taskContextPath,
|
|
85
|
+
claudeMdUpdated,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Refresh TASK_CONTEXT.md with latest data from Damper (for resume scenarios)
|
|
90
|
+
*/
|
|
91
|
+
export async function refreshContext(options) {
|
|
92
|
+
// For now, just run bootstrap again - it will overwrite with fresh data
|
|
93
|
+
return bootstrapContext(options);
|
|
94
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
export interface Task {
|
|
2
|
+
id: string;
|
|
3
|
+
title: string;
|
|
4
|
+
type: string;
|
|
5
|
+
status: string;
|
|
6
|
+
priority: string;
|
|
7
|
+
effort?: string | null;
|
|
8
|
+
quarter?: string | null;
|
|
9
|
+
labels?: string[];
|
|
10
|
+
dueDate?: string | null;
|
|
11
|
+
feedbackCount: number;
|
|
12
|
+
hasImplementationPlan: boolean;
|
|
13
|
+
subtaskProgress?: {
|
|
14
|
+
done: number;
|
|
15
|
+
total: number;
|
|
16
|
+
} | null;
|
|
17
|
+
}
|
|
18
|
+
export interface TaskDetail extends Task {
|
|
19
|
+
description?: string | null;
|
|
20
|
+
implementationPlan?: string | null;
|
|
21
|
+
voteScore: number;
|
|
22
|
+
subtasks?: Array<{
|
|
23
|
+
id: string;
|
|
24
|
+
title: string;
|
|
25
|
+
done: boolean;
|
|
26
|
+
}>;
|
|
27
|
+
agentNotes?: string | null;
|
|
28
|
+
commits?: Array<{
|
|
29
|
+
hash: string;
|
|
30
|
+
message: string;
|
|
31
|
+
}>;
|
|
32
|
+
feedback: Array<{
|
|
33
|
+
id: string;
|
|
34
|
+
title: string;
|
|
35
|
+
description: string;
|
|
36
|
+
voterCount: number;
|
|
37
|
+
}>;
|
|
38
|
+
lockedBy?: string | null;
|
|
39
|
+
lockedAt?: string | null;
|
|
40
|
+
}
|
|
41
|
+
export interface ContextSection {
|
|
42
|
+
section: string;
|
|
43
|
+
content: string;
|
|
44
|
+
updatedAt: string;
|
|
45
|
+
source: string;
|
|
46
|
+
tags?: string[];
|
|
47
|
+
appliesTo?: string[];
|
|
48
|
+
}
|
|
49
|
+
export interface ContextIndex {
|
|
50
|
+
isEmpty: boolean;
|
|
51
|
+
index: Array<{
|
|
52
|
+
section: string;
|
|
53
|
+
preview: string;
|
|
54
|
+
updatedAt: string;
|
|
55
|
+
tags?: string[];
|
|
56
|
+
appliesTo?: string[];
|
|
57
|
+
}>;
|
|
58
|
+
relevantSections?: string[];
|
|
59
|
+
hint?: string;
|
|
60
|
+
criticalRules?: string[];
|
|
61
|
+
}
|
|
62
|
+
export interface Template {
|
|
63
|
+
name: string;
|
|
64
|
+
description?: string | null;
|
|
65
|
+
content: string;
|
|
66
|
+
filePattern?: string | null;
|
|
67
|
+
tags?: string[];
|
|
68
|
+
updatedAt: string;
|
|
69
|
+
}
|
|
70
|
+
export interface Module {
|
|
71
|
+
name: string;
|
|
72
|
+
path: string;
|
|
73
|
+
port?: number | null;
|
|
74
|
+
dependsOn?: string[];
|
|
75
|
+
description?: string | null;
|
|
76
|
+
tags?: string[];
|
|
77
|
+
updatedAt: string;
|
|
78
|
+
}
|
|
79
|
+
export interface AgentInstructions {
|
|
80
|
+
format: 'markdown' | 'section';
|
|
81
|
+
content: string;
|
|
82
|
+
lastModified: string;
|
|
83
|
+
}
|
|
84
|
+
export interface StartTaskResult {
|
|
85
|
+
id: string;
|
|
86
|
+
status: string;
|
|
87
|
+
message: string;
|
|
88
|
+
context?: ContextIndex;
|
|
89
|
+
}
|
|
90
|
+
export interface LockConflictError extends Error {
|
|
91
|
+
lockInfo: {
|
|
92
|
+
error: string;
|
|
93
|
+
lockedBy: string;
|
|
94
|
+
lockedAt: string;
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
export declare class DamperApi {
|
|
98
|
+
private apiKey;
|
|
99
|
+
constructor(apiKey: string);
|
|
100
|
+
private request;
|
|
101
|
+
listTasks(filters?: {
|
|
102
|
+
status?: 'planned' | 'in_progress' | 'done' | 'all';
|
|
103
|
+
type?: 'bug' | 'feature' | 'improvement' | 'task';
|
|
104
|
+
quarter?: string;
|
|
105
|
+
sort?: 'importance' | 'newest' | 'votes';
|
|
106
|
+
limit?: number;
|
|
107
|
+
}): Promise<{
|
|
108
|
+
project: string;
|
|
109
|
+
tasks: Task[];
|
|
110
|
+
}>;
|
|
111
|
+
getTask(taskId: string): Promise<TaskDetail>;
|
|
112
|
+
startTask(taskId: string, force?: boolean): Promise<StartTaskResult>;
|
|
113
|
+
getProjectContext(taskId?: string): Promise<ContextIndex>;
|
|
114
|
+
getContextSection(section: string): Promise<ContextSection | {
|
|
115
|
+
pattern: string;
|
|
116
|
+
sections: ContextSection[];
|
|
117
|
+
}>;
|
|
118
|
+
listContextSections(): Promise<{
|
|
119
|
+
sections: Array<{
|
|
120
|
+
section: string;
|
|
121
|
+
updatedAt: string;
|
|
122
|
+
source: string;
|
|
123
|
+
preview: string;
|
|
124
|
+
tags?: string[];
|
|
125
|
+
appliesTo?: string[];
|
|
126
|
+
}>;
|
|
127
|
+
isEmpty: boolean;
|
|
128
|
+
}>;
|
|
129
|
+
listTemplates(): Promise<{
|
|
130
|
+
templates: Array<{
|
|
131
|
+
name: string;
|
|
132
|
+
description?: string | null;
|
|
133
|
+
filePattern?: string | null;
|
|
134
|
+
tags?: string[];
|
|
135
|
+
preview: string;
|
|
136
|
+
updatedAt: string;
|
|
137
|
+
}>;
|
|
138
|
+
isEmpty: boolean;
|
|
139
|
+
}>;
|
|
140
|
+
getTemplate(name: string): Promise<Template>;
|
|
141
|
+
listModules(): Promise<{
|
|
142
|
+
modules: Module[];
|
|
143
|
+
isEmpty: boolean;
|
|
144
|
+
}>;
|
|
145
|
+
getModule(name: string): Promise<Module>;
|
|
146
|
+
getAgentInstructions(format?: 'markdown' | 'section'): Promise<AgentInstructions>;
|
|
147
|
+
getLinkedFeedback(taskId: string): Promise<Array<{
|
|
148
|
+
id: string;
|
|
149
|
+
title: string;
|
|
150
|
+
description: string;
|
|
151
|
+
voterCount: number;
|
|
152
|
+
}>>;
|
|
153
|
+
}
|
|
154
|
+
export declare function createDamperApi(apiKey?: string): DamperApi;
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
// Config
|
|
2
|
+
const API_URL = process.env.DAMPER_API_URL || 'https://api.usedamper.com';
|
|
3
|
+
// API Client class
|
|
4
|
+
export class DamperApi {
|
|
5
|
+
apiKey;
|
|
6
|
+
constructor(apiKey) {
|
|
7
|
+
this.apiKey = apiKey;
|
|
8
|
+
}
|
|
9
|
+
async request(method, path, body) {
|
|
10
|
+
const res = await fetch(`${API_URL}${path}`, {
|
|
11
|
+
method,
|
|
12
|
+
headers: {
|
|
13
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
14
|
+
'Content-Type': 'application/json',
|
|
15
|
+
},
|
|
16
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
17
|
+
});
|
|
18
|
+
if (!res.ok) {
|
|
19
|
+
const err = await res.json().catch(() => ({}));
|
|
20
|
+
// Preserve lock info for 409 conflicts
|
|
21
|
+
if (res.status === 409 && err.lockedBy) {
|
|
22
|
+
const lockErr = new Error(err.error || `HTTP ${res.status}`);
|
|
23
|
+
lockErr.lockInfo = err;
|
|
24
|
+
throw lockErr;
|
|
25
|
+
}
|
|
26
|
+
throw new Error(err.error || `HTTP ${res.status}`);
|
|
27
|
+
}
|
|
28
|
+
return res.json();
|
|
29
|
+
}
|
|
30
|
+
// Tasks
|
|
31
|
+
async listTasks(filters) {
|
|
32
|
+
const params = new URLSearchParams();
|
|
33
|
+
if (filters?.status)
|
|
34
|
+
params.set('status', filters.status);
|
|
35
|
+
if (filters?.type)
|
|
36
|
+
params.set('type', filters.type);
|
|
37
|
+
if (filters?.quarter)
|
|
38
|
+
params.set('quarter', filters.quarter);
|
|
39
|
+
if (filters?.sort)
|
|
40
|
+
params.set('sort', filters.sort);
|
|
41
|
+
if (filters?.limit)
|
|
42
|
+
params.set('limit', String(filters.limit));
|
|
43
|
+
const query = params.toString();
|
|
44
|
+
const data = await this.request('GET', `/api/agent/tasks${query ? `?${query}` : ''}`);
|
|
45
|
+
return { project: data.project.name, tasks: data.tasks };
|
|
46
|
+
}
|
|
47
|
+
async getTask(taskId) {
|
|
48
|
+
return this.request('GET', `/api/agent/tasks/${taskId}`);
|
|
49
|
+
}
|
|
50
|
+
async startTask(taskId, force) {
|
|
51
|
+
return this.request('POST', `/api/agent/tasks/${taskId}/start`, force ? { force: true } : undefined);
|
|
52
|
+
}
|
|
53
|
+
// Project Context
|
|
54
|
+
async getProjectContext(taskId) {
|
|
55
|
+
const params = taskId ? `?task_id=${taskId}` : '';
|
|
56
|
+
return this.request('GET', `/api/agent/context/index${params}`);
|
|
57
|
+
}
|
|
58
|
+
async getContextSection(section) {
|
|
59
|
+
// Encode each path segment individually but preserve slashes
|
|
60
|
+
const encodedSection = section
|
|
61
|
+
.split('/')
|
|
62
|
+
.map(part => encodeURIComponent(part).replace(/\*/g, '%2A'))
|
|
63
|
+
.join('/');
|
|
64
|
+
return this.request('GET', `/api/agent/context/${encodedSection}`);
|
|
65
|
+
}
|
|
66
|
+
async listContextSections() {
|
|
67
|
+
return this.request('GET', '/api/agent/context');
|
|
68
|
+
}
|
|
69
|
+
// Templates
|
|
70
|
+
async listTemplates() {
|
|
71
|
+
return this.request('GET', '/api/agent/templates');
|
|
72
|
+
}
|
|
73
|
+
async getTemplate(name) {
|
|
74
|
+
return this.request('GET', `/api/agent/templates/${name}`);
|
|
75
|
+
}
|
|
76
|
+
// Modules
|
|
77
|
+
async listModules() {
|
|
78
|
+
return this.request('GET', '/api/agent/modules');
|
|
79
|
+
}
|
|
80
|
+
async getModule(name) {
|
|
81
|
+
return this.request('GET', `/api/agent/modules/${name}`);
|
|
82
|
+
}
|
|
83
|
+
// Agent Instructions
|
|
84
|
+
async getAgentInstructions(format = 'section') {
|
|
85
|
+
// The MCP server has this hardcoded, so we'll return the same content
|
|
86
|
+
const lastModified = '2025-02-05';
|
|
87
|
+
const section = `## Task Management with Damper MCP
|
|
88
|
+
|
|
89
|
+
> Last updated: ${lastModified}
|
|
90
|
+
|
|
91
|
+
This project uses Damper MCP for task tracking. **You MUST follow this workflow.**
|
|
92
|
+
|
|
93
|
+
### At Session Start (MANDATORY)
|
|
94
|
+
1. \`get_project_context\` - **READ THIS FIRST.** Contains architecture, conventions, and critical project info.
|
|
95
|
+
2. \`get_context_section\` - Fetch full content for sections relevant to your task
|
|
96
|
+
3. \`list_tasks\` - Check for existing tasks to work on
|
|
97
|
+
4. If working on a task: \`start_task\` to lock it
|
|
98
|
+
|
|
99
|
+
### While Working
|
|
100
|
+
- \`add_commit\` after each commit with hash and message
|
|
101
|
+
- \`add_note\` for decisions: "Decision: chose X because Y"
|
|
102
|
+
- \`update_subtask\` to mark subtask progress
|
|
103
|
+
- **Follow patterns from project context** - Don't reinvent; use existing conventions
|
|
104
|
+
|
|
105
|
+
### Feedback & Changelog Integration
|
|
106
|
+
- \`link_feedback_to_task\` - Link user feedback IDs to your task (helps track what customer requests led to the feature)
|
|
107
|
+
- When completing a **public** task, it auto-adds to the project's draft changelog
|
|
108
|
+
- \`list_changelogs\` - View existing changelogs and drafts
|
|
109
|
+
- \`create_changelog\` / \`update_changelog\` - Create or edit changelog entries (use \`summary\` for social-friendly posts)
|
|
110
|
+
- \`add_to_changelog\` - Manually add completed tasks to a changelog
|
|
111
|
+
- \`publish_changelog\` - Publish with optional email notifications and Twitter posting
|
|
112
|
+
|
|
113
|
+
### At Session End (MANDATORY)
|
|
114
|
+
- ALWAYS call \`add_note\` with session summary before stopping
|
|
115
|
+
- ALWAYS call \`complete_task\` (if done) or \`abandon_task\` (if stopping early)
|
|
116
|
+
- NEVER leave a started task without completing or abandoning it
|
|
117
|
+
- If you learned something about the codebase, consider updating project context
|
|
118
|
+
|
|
119
|
+
### Why This Matters
|
|
120
|
+
- **Project context prevents mistakes** - Contains architecture decisions, gotchas, and patterns
|
|
121
|
+
- Locked tasks block other agents from working on them
|
|
122
|
+
- Commits and notes help the next agent continue the work
|
|
123
|
+
- Updating context saves future agents from re-analyzing the codebase
|
|
124
|
+
- Linking feedback connects customer requests to shipped features`;
|
|
125
|
+
if (format === 'markdown') {
|
|
126
|
+
return {
|
|
127
|
+
format: 'markdown',
|
|
128
|
+
content: `# CLAUDE.md\n\n${section}`,
|
|
129
|
+
lastModified,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
return {
|
|
133
|
+
format: 'section',
|
|
134
|
+
content: section,
|
|
135
|
+
lastModified,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
// Feedback
|
|
139
|
+
async getLinkedFeedback(taskId) {
|
|
140
|
+
const task = await this.getTask(taskId);
|
|
141
|
+
return task.feedback || [];
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
// Factory function for creating API client
|
|
145
|
+
export function createDamperApi(apiKey) {
|
|
146
|
+
const key = apiKey || process.env.DAMPER_API_KEY;
|
|
147
|
+
if (!key) {
|
|
148
|
+
throw new Error('DAMPER_API_KEY required. Get one from Damper → Settings → API Keys');
|
|
149
|
+
}
|
|
150
|
+
return new DamperApi(key);
|
|
151
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface WorktreeState {
|
|
2
|
+
taskId: string;
|
|
3
|
+
path: string;
|
|
4
|
+
branch: string;
|
|
5
|
+
projectRoot: string;
|
|
6
|
+
createdAt: string;
|
|
7
|
+
}
|
|
8
|
+
export declare function getWorktrees(): WorktreeState[];
|
|
9
|
+
export declare function getWorktreeByTaskId(taskId: string): WorktreeState | undefined;
|
|
10
|
+
export declare function getWorktreeByPath(worktreePath: string): WorktreeState | undefined;
|
|
11
|
+
export declare function addWorktree(worktree: WorktreeState): void;
|
|
12
|
+
export declare function removeWorktree(taskId: string): void;
|
|
13
|
+
export declare function removeWorktreeByPath(worktreePath: string): void;
|
|
14
|
+
export declare function cleanupStaleWorktrees(): WorktreeState[];
|
|
15
|
+
export declare function getWorktreesForProject(projectRoot: string): WorktreeState[];
|