@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.
@@ -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[];