@damper/cli 0.6.10 → 0.6.12

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,6 +5,7 @@ import pc from 'picocolors';
5
5
  import { getWorktrees, cleanupStaleWorktrees, removeWorktree } from '../services/state.js';
6
6
  import { createDamperApi } from '../services/damper-api.js';
7
7
  import { removeWorktreeDir } from '../services/worktree.js';
8
+ import { shortId, shortIdRaw, formatTaskLine } from '../ui/format.js';
8
9
  async function hasUncommittedChanges(worktreePath) {
9
10
  try {
10
11
  const { stdout } = await execa('git', ['status', '--porcelain'], {
@@ -108,10 +109,7 @@ export async function cleanupCommand() {
108
109
  console.log(pc.bold('\nWorktrees available for cleanup:\n'));
109
110
  const choices = candidates.map(c => {
110
111
  const worktreeName = c.worktree.path.split('/').pop() || c.worktree.path;
111
- let description = `#${c.worktree.taskId}`;
112
- if (c.task) {
113
- description += ` - ${c.task.title}`;
114
- }
112
+ let description = c.task ? formatTaskLine(c.task) : shortId(c.worktree.taskId);
115
113
  let reasonBadge = '';
116
114
  switch (c.reason) {
117
115
  case 'completed':
@@ -177,7 +175,7 @@ export async function cleanupCommand() {
177
175
  if (candidate.reason === 'in_progress' && api) {
178
176
  try {
179
177
  await api.abandonTask(worktree.taskId, 'Dropped via CLI cleanup');
180
- console.log(pc.green(`✓ Released task #${worktree.taskId}`));
178
+ console.log(pc.green(`✓ Released task #${shortIdRaw(worktree.taskId)}`));
181
179
  }
182
180
  catch (err) {
183
181
  const error = err;
@@ -4,6 +4,7 @@ import { createDamperApi } from '../services/damper-api.js';
4
4
  import { getWorktrees, removeWorktree } from '../services/state.js';
5
5
  import { removeWorktreeDir, getGitRoot } from '../services/worktree.js';
6
6
  import { getApiKey } from '../services/config.js';
7
+ import { shortId, shortIdRaw, getTypeIcon, getPriorityIcon, formatEffort } from '../ui/format.js';
7
8
  export async function releaseCommand() {
8
9
  // Get project root
9
10
  let projectRoot;
@@ -34,10 +35,14 @@ export async function releaseCommand() {
34
35
  const worktrees = getWorktrees();
35
36
  const choices = tasks.map(task => {
36
37
  const worktree = worktrees.find(w => w.taskId === task.id);
38
+ const priorityIcon = getPriorityIcon(task.priority);
39
+ const typeIcon = getTypeIcon(task.type);
40
+ const id = shortId(task.id);
41
+ const effort = formatEffort(task.effort);
37
42
  const lockedInfo = task.lockedBy ? pc.dim(` (locked by ${task.lockedBy})`) : '';
38
43
  const worktreeInfo = worktree ? pc.dim(` [has worktree]`) : '';
39
44
  return {
40
- name: `#${task.id} ${task.title}${lockedInfo}${worktreeInfo}`,
45
+ name: `${priorityIcon}${typeIcon} ${id} ${task.title}${effort ? ` ${effort}` : ''}${lockedInfo}${worktreeInfo}`,
41
46
  value: { task, worktree },
42
47
  };
43
48
  });
@@ -49,7 +54,7 @@ export async function releaseCommand() {
49
54
  // Confirm
50
55
  console.log();
51
56
  console.log(pc.yellow(`This will:`));
52
- console.log(pc.dim(` • Release the lock on task #${task.id}`));
57
+ console.log(pc.dim(` • Release the lock on task #${shortIdRaw(task.id)}`));
53
58
  console.log(pc.dim(` • Set task status back to "planned"`));
54
59
  if (worktree) {
55
60
  console.log(pc.dim(` • Optionally remove the worktree`));
@@ -66,7 +71,7 @@ export async function releaseCommand() {
66
71
  // Abandon the task in Damper
67
72
  try {
68
73
  await api.abandonTask(task.id, 'Released via CLI');
69
- console.log(pc.green(`\n✓ Task #${task.id} released to planned status`));
74
+ console.log(pc.green(`\n✓ Task #${shortIdRaw(task.id)} released to planned status`));
70
75
  }
71
76
  catch (err) {
72
77
  const error = err;
@@ -7,6 +7,7 @@ import { pickTask } from '../ui/task-picker.js';
7
7
  import { launchClaude, postTaskFlow, isClaudeInstalled, isDamperMcpConfigured, configureDamperMcp } from '../services/claude.js';
8
8
  import { getWorktreesForProject, cleanupStaleWorktrees } from '../services/state.js';
9
9
  import { getApiKey, isProjectConfigured, getProjectConfigPath } from '../services/config.js';
10
+ import { shortIdRaw } from '../ui/format.js';
10
11
  export async function startCommand(options) {
11
12
  // Check Claude CLI first
12
13
  const claudeInstalled = await isClaudeInstalled();
@@ -68,10 +69,10 @@ export async function startCommand(options) {
68
69
  if (existingWorktree && fs.existsSync(existingWorktree.path)) {
69
70
  isResume = true;
70
71
  worktreePath = existingWorktree.path;
71
- console.log(pc.cyan(`\nResuming task #${taskId}: ${taskTitle}`));
72
+ console.log(pc.cyan(`\nResuming task #${shortIdRaw(taskId)}: ${taskTitle}`));
72
73
  }
73
74
  else {
74
- console.log(pc.cyan(`\nStarting task #${taskId}: ${taskTitle}`));
75
+ console.log(pc.cyan(`\nStarting task #${shortIdRaw(taskId)}: ${taskTitle}`));
75
76
  }
76
77
  }
77
78
  else {
@@ -95,7 +96,7 @@ export async function startCommand(options) {
95
96
  }
96
97
  if (isResume && worktreePath) {
97
98
  // Resume existing worktree
98
- console.log(pc.green(`\n✓ Resuming: #${taskId} ${taskTitle}`));
99
+ console.log(pc.green(`\n✓ Resuming: #${shortIdRaw(taskId)} ${taskTitle}`));
99
100
  console.log(pc.dim(` Worktree: ${worktreePath}`));
100
101
  // Refresh context with latest from Damper
101
102
  console.log(pc.dim('\nRefreshing context from Damper...'));
@@ -3,6 +3,7 @@ import pc from 'picocolors';
3
3
  import { getWorktrees, cleanupStaleWorktrees } from '../services/state.js';
4
4
  import { createDamperApi } from '../services/damper-api.js';
5
5
  import { getGitRoot } from '../services/worktree.js';
6
+ import { shortId, getTypeIcon, getPriorityIcon, formatEffort, formatSubtaskProgress, relativeTime, statusColor as getStatusColor, } from '../ui/format.js';
6
7
  export async function statusCommand() {
7
8
  // Clean up stale entries first
8
9
  const stale = cleanupStaleWorktrees();
@@ -49,24 +50,43 @@ export async function statusCommand() {
49
50
  console.log();
50
51
  for (const wt of projectWorktrees) {
51
52
  const exists = fs.existsSync(wt.path);
52
- const worktreeName = wt.path.split('/').pop() || wt.path;
53
- // Try to get task status
54
- let taskStatus = '';
53
+ const statusIcon = exists ? pc.green('') : pc.red('○');
54
+ // Try to get task details from Damper
55
55
  if (api) {
56
56
  try {
57
57
  const task = await api.getTask(wt.taskId);
58
- const statusColor = task.status === 'done' ? pc.green : task.status === 'in_progress' ? pc.yellow : pc.dim;
59
- taskStatus = statusColor(`[${task.status}]`);
58
+ const priorityIcon = getPriorityIcon(task.priority);
59
+ const typeIcon = getTypeIcon(task.type);
60
+ const colorFn = getStatusColor(task.status);
61
+ const statusBadge = colorFn(`[${task.status}]`);
62
+ const effort = formatEffort(task.effort);
63
+ const progress = formatSubtaskProgress(task.subtaskProgress);
64
+ const time = relativeTime(wt.createdAt);
65
+ console.log(` ${statusIcon} ${priorityIcon}${typeIcon} ${shortId(task.id)} ${task.title} ${statusBadge}${effort ? ` ${effort}` : ''}`);
66
+ const metaParts = [wt.branch];
67
+ if (time)
68
+ metaParts.push(time);
69
+ if (progress)
70
+ metaParts.push(progress);
71
+ console.log(` ${metaParts.join(` ${pc.dim('\u00B7')} `)}`);
60
72
  }
61
73
  catch {
62
- taskStatus = pc.dim('[unknown]');
74
+ console.log(` ${statusIcon} ${shortId(wt.taskId)} ${pc.dim('[unknown]')}`);
75
+ const time = relativeTime(wt.createdAt);
76
+ const metaParts = [wt.branch];
77
+ if (time)
78
+ metaParts.push(time);
79
+ console.log(` ${metaParts.join(` ${pc.dim('\u00B7')} `)}`);
63
80
  }
64
81
  }
65
- const statusIcon = exists ? pc.green('●') : pc.red('○');
66
- console.log(` ${statusIcon} ${pc.cyan(`#${wt.taskId}`)} ${taskStatus}`);
67
- console.log(` ${pc.dim('Path:')} ${worktreeName}`);
68
- console.log(` ${pc.dim('Branch:')} ${wt.branch}`);
69
- console.log(` ${pc.dim('Created:')} ${new Date(wt.createdAt).toLocaleDateString()}`);
82
+ else {
83
+ console.log(` ${statusIcon} ${shortId(wt.taskId)}`);
84
+ const time = relativeTime(wt.createdAt);
85
+ const metaParts = [wt.branch];
86
+ if (time)
87
+ metaParts.push(time);
88
+ console.log(` ${metaParts.join(` ${pc.dim('\u00B7')} `)}`);
89
+ }
70
90
  console.log();
71
91
  }
72
92
  }
@@ -4,6 +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
8
  const CLAUDE_SETTINGS_DIR = path.join(os.homedir(), '.claude');
8
9
  const CLAUDE_SETTINGS_FILE = path.join(CLAUDE_SETTINGS_DIR, 'settings.json');
9
10
  /**
@@ -85,7 +86,7 @@ export function configureDamperMcp() {
85
86
  */
86
87
  export async function launchClaude(options) {
87
88
  const { cwd, taskId, taskTitle, apiKey, yolo } = options;
88
- console.log(pc.green(`\nStarting Claude Code for task #${taskId}: ${taskTitle}`));
89
+ console.log(pc.green(`\nStarting Claude Code for task #${shortIdRaw(taskId)}: ${taskTitle}`));
89
90
  console.log(pc.dim(`Directory: ${cwd}`));
90
91
  console.log();
91
92
  // Show workflow explanation
@@ -0,0 +1,30 @@
1
+ import type { Task } from '../services/damper-api.js';
2
+ /** Colored short ID: dim `#` + cyan first 8 chars */
3
+ export declare function shortId(id: string): string;
4
+ /** Plain 8-char slice (for use inside other color wrappers) */
5
+ export declare function shortIdRaw(id: string): string;
6
+ /** Type emoji icon */
7
+ export declare function getTypeIcon(type: string): string;
8
+ /** Priority prefix: red/yellow circle + space, or empty */
9
+ export declare function getPriorityIcon(priority: string): string;
10
+ /** Dim effort badge like `▪ L`, or empty string */
11
+ export declare function formatEffort(effort: string | null | undefined): string;
12
+ /** Dim feedback count like `💬 3`, or empty string */
13
+ export declare function formatFeedback(count: number | null | undefined): string;
14
+ /** Dim labels joined with ` · `, or empty string */
15
+ export declare function formatLabels(labels: string[] | null | undefined): string;
16
+ /** 10-char progress bar: green filled, dim empty, with count */
17
+ export declare function formatSubtaskProgress(progress: {
18
+ done: number;
19
+ total: number;
20
+ } | null | undefined): string;
21
+ /** Relative time string from date, no dependencies */
22
+ export declare function relativeTime(date: string | Date | null | undefined): string;
23
+ /** Returns a picocolors function for the given status */
24
+ export declare function statusColor(status: string): (s: string) => string;
25
+ /** Box-drawing section header: `┌─ Label ───────────────┐` */
26
+ export declare function sectionHeader(label: string, width?: number): string;
27
+ /** Compact one-liner: `🔴 ✨ #a1b2c3d4 Title ▪ L` */
28
+ export declare function formatTaskLine(task: Task): string;
29
+ /** Dim metadata second line with segments joined by ` · ` */
30
+ export declare function formatTaskMeta(task: Task): string;
@@ -0,0 +1,139 @@
1
+ import pc from 'picocolors';
2
+ /** Colored short ID: dim `#` + cyan first 8 chars */
3
+ export function shortId(id) {
4
+ return `${pc.dim('#')}${pc.cyan(id.slice(0, 8))}`;
5
+ }
6
+ /** Plain 8-char slice (for use inside other color wrappers) */
7
+ export function shortIdRaw(id) {
8
+ return id.slice(0, 8);
9
+ }
10
+ /** Type emoji icon */
11
+ export function getTypeIcon(type) {
12
+ switch (type) {
13
+ case 'bug': return '\u{1F41B}';
14
+ case 'feature': return '\u{2728}';
15
+ case 'improvement': return '\u{1F4A1}';
16
+ default: return '\u{1F4CC}';
17
+ }
18
+ }
19
+ /** Priority prefix: red/yellow circle + space, or empty */
20
+ export function getPriorityIcon(priority) {
21
+ switch (priority) {
22
+ case 'high': return '\u{1F534} ';
23
+ case 'medium': return '\u{1F7E1} ';
24
+ default: return '';
25
+ }
26
+ }
27
+ /** Dim effort badge like `▪ L`, or empty string */
28
+ export function formatEffort(effort) {
29
+ if (!effort)
30
+ return '';
31
+ return pc.dim(`\u25AA ${effort.toUpperCase()}`);
32
+ }
33
+ /** Dim feedback count like `💬 3`, or empty string */
34
+ export function formatFeedback(count) {
35
+ if (!count)
36
+ return '';
37
+ return pc.dim(`\u{1F4AC} ${count}`);
38
+ }
39
+ /** Dim labels joined with ` · `, or empty string */
40
+ export function formatLabels(labels) {
41
+ if (!labels || labels.length === 0)
42
+ return '';
43
+ return pc.dim(labels.join(' \u00B7 '));
44
+ }
45
+ /** 10-char progress bar: green filled, dim empty, with count */
46
+ export function formatSubtaskProgress(progress) {
47
+ if (!progress || progress.total === 0)
48
+ return '';
49
+ const { done, total } = progress;
50
+ const width = 10;
51
+ const filled = Math.round((done / total) * width);
52
+ const empty = width - filled;
53
+ const bar = pc.green('\u2501'.repeat(filled)) + pc.dim('\u2591'.repeat(empty));
54
+ return `${bar} ${done}/${total}`;
55
+ }
56
+ /** Relative time string from date, no dependencies */
57
+ export function relativeTime(date) {
58
+ if (!date)
59
+ return '';
60
+ const now = Date.now();
61
+ const then = date instanceof Date ? date.getTime() : new Date(date).getTime();
62
+ if (isNaN(then))
63
+ return '';
64
+ const diffMs = now - then;
65
+ if (diffMs < 0)
66
+ return 'just now';
67
+ const seconds = Math.floor(diffMs / 1000);
68
+ if (seconds < 60)
69
+ return 'just now';
70
+ const minutes = Math.floor(seconds / 60);
71
+ if (minutes < 60)
72
+ return `${minutes}m ago`;
73
+ const hours = Math.floor(minutes / 60);
74
+ if (hours < 24)
75
+ return `${hours}h ago`;
76
+ const days = Math.floor(hours / 24);
77
+ if (days < 14)
78
+ return `${days}d ago`;
79
+ const weeks = Math.floor(days / 7);
80
+ if (weeks < 8)
81
+ return `${weeks}w ago`;
82
+ const months = Math.floor(days / 30);
83
+ if (months < 12)
84
+ return `${months}mo ago`;
85
+ const years = Math.floor(days / 365);
86
+ return `${years}y ago`;
87
+ }
88
+ /** Returns a picocolors function for the given status */
89
+ export function statusColor(status) {
90
+ switch (status) {
91
+ case 'done': return pc.green;
92
+ case 'in_progress': return pc.yellow;
93
+ default: return pc.dim;
94
+ }
95
+ }
96
+ /** Box-drawing section header: `┌─ Label ───────────────┐` */
97
+ export function sectionHeader(label, width = 40) {
98
+ const inner = `\u2500 ${label} `;
99
+ const remaining = Math.max(0, width - inner.length - 2); // 2 for ┌ and ┐
100
+ return `\u250C${inner}${'\u2500'.repeat(remaining)}\u2510`;
101
+ }
102
+ /** Compact one-liner: `🔴 ✨ #a1b2c3d4 Title ▪ L` */
103
+ export function formatTaskLine(task) {
104
+ const priority = getPriorityIcon(task.priority);
105
+ const icon = getTypeIcon(task.type);
106
+ const id = shortId(task.id);
107
+ const effort = formatEffort(task.effort);
108
+ const parts = [
109
+ `${priority}${icon} ${id} ${task.title}`,
110
+ ];
111
+ if (effort)
112
+ parts.push(effort);
113
+ return parts.join(' ');
114
+ }
115
+ /** Dim metadata second line with segments joined by ` · ` */
116
+ export function formatTaskMeta(task) {
117
+ const segments = [];
118
+ if (task.quarter) {
119
+ segments.push(pc.dim(task.quarter));
120
+ }
121
+ if (task.hasImplementationPlan) {
122
+ segments.push(pc.dim('has plan'));
123
+ }
124
+ const progress = formatSubtaskProgress(task.subtaskProgress);
125
+ if (progress) {
126
+ segments.push(progress);
127
+ }
128
+ const feedback = formatFeedback(task.feedbackCount);
129
+ if (feedback) {
130
+ segments.push(feedback);
131
+ }
132
+ const labels = formatLabels(task.labels);
133
+ if (labels) {
134
+ segments.push(labels);
135
+ }
136
+ if (segments.length === 0)
137
+ return '';
138
+ return segments.join(` ${pc.dim('\u00B7')} `);
139
+ }
@@ -1,52 +1,35 @@
1
1
  import { select, confirm, input, Separator } from '@inquirer/prompts';
2
2
  import pc from 'picocolors';
3
- function getTypeIcon(type) {
4
- switch (type) {
5
- case 'bug': return pc.red('bug');
6
- case 'feature': return pc.green('feature');
7
- case 'improvement': return pc.blue('improvement');
8
- default: return pc.gray('task');
9
- }
10
- }
11
- function getPriorityIcon(priority) {
12
- switch (priority) {
13
- case 'high': return pc.red('!');
14
- case 'medium': return pc.yellow('~');
15
- default: return pc.dim('-');
16
- }
17
- }
3
+ import { shortId, shortIdRaw, getTypeIcon, getPriorityIcon, formatEffort, formatTaskMeta, sectionHeader, relativeTime, } from './format.js';
18
4
  function formatTaskChoice(choice) {
19
5
  const { task } = choice;
20
- const typeIcon = getTypeIcon(task.type);
21
6
  const priorityIcon = getPriorityIcon(task.priority);
7
+ const typeIcon = getTypeIcon(task.type);
8
+ const id = shortId(task.id);
9
+ const effort = formatEffort(task.effort);
10
+ const firstLine = `${priorityIcon}${typeIcon} ${id} ${task.title}${effort ? ` ${effort}` : ''}`;
22
11
  if (choice.type === 'in_progress') {
23
12
  const worktreeName = choice.worktree.path.split('/').pop() || choice.worktree.path;
24
- let description = `${pc.dim('#')}${pc.cyan(task.id)} ${task.title} ${pc.dim(`[${typeIcon}]`)} ${priorityIcon}`;
25
- description += `\n ${pc.dim(`Worktree: ${worktreeName}`)}`;
13
+ const time = relativeTime(choice.worktree.createdAt);
14
+ const metaParts = [worktreeName];
15
+ if (time)
16
+ metaParts.push(time);
17
+ let description = `${firstLine}\n ${pc.dim(metaParts.join(' \u00B7 '))}`;
26
18
  if (choice.lastNote) {
27
- // Truncate long notes
28
19
  const note = choice.lastNote.length > 60 ? choice.lastNote.slice(0, 60) + '...' : choice.lastNote;
29
- description += `\n ${pc.dim(`Last: "${note}"`)}`;
20
+ description += `\n ${pc.dim(`Last: "${note}"`)}`;
30
21
  }
31
22
  return description;
32
23
  }
33
24
  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;
25
+ return `${firstLine}\n ${pc.yellow(`locked by ${choice.lockedBy}`)}`;
37
26
  }
38
27
  // Available task
39
- let description = `${pc.dim('#')}${pc.cyan(task.id)} ${task.title} ${pc.dim(`[${typeIcon}]`)} ${priorityIcon}`;
40
- if (task.quarter) {
41
- description += ` ${pc.dim(task.quarter)}`;
42
- }
43
- if (task.hasImplementationPlan) {
44
- description += pc.dim(' [plan]');
45
- }
46
- if (task.subtaskProgress) {
47
- description += pc.dim(` [${task.subtaskProgress.done}/${task.subtaskProgress.total}]`);
28
+ const meta = formatTaskMeta(task);
29
+ if (meta) {
30
+ return `${firstLine}\n ${meta}`;
48
31
  }
49
- return description;
32
+ return firstLine;
50
33
  }
51
34
  export async function pickTask(options) {
52
35
  const { api, worktrees, typeFilter, statusFilter } = options;
@@ -97,7 +80,7 @@ export async function pickTask(options) {
97
80
  }
98
81
  const choices = [];
99
82
  if (inProgressChoices.length > 0) {
100
- choices.push(new Separator(pc.bold('\n--- In Progress (your worktrees) ---')));
83
+ choices.push(new Separator(`\n${sectionHeader(`In Progress (${inProgressChoices.length})`)}`));
101
84
  for (const choice of inProgressChoices) {
102
85
  choices.push({
103
86
  name: formatTaskChoice(choice),
@@ -106,7 +89,7 @@ export async function pickTask(options) {
106
89
  }
107
90
  }
108
91
  if (availableChoices.length > 0) {
109
- choices.push(new Separator(pc.bold('\n--- Available Tasks ---')));
92
+ choices.push(new Separator(`\n${sectionHeader(`Available (${availableChoices.length})`)}`));
110
93
  for (const choice of availableChoices) {
111
94
  choices.push({
112
95
  name: formatTaskChoice(choice),
@@ -115,7 +98,7 @@ export async function pickTask(options) {
115
98
  }
116
99
  }
117
100
  if (lockedChoices.length > 0) {
118
- choices.push(new Separator(pc.bold('\n--- Locked Tasks (can take over) ---')));
101
+ choices.push(new Separator(`\n${sectionHeader(`Locked (${lockedChoices.length})`)}`));
119
102
  for (const choice of lockedChoices) {
120
103
  choices.push({
121
104
  name: formatTaskChoice(choice),
@@ -139,7 +122,7 @@ export async function pickTask(options) {
139
122
  const selected = await select({
140
123
  message: 'Select a task to work on:',
141
124
  choices: choices,
142
- pageSize: 15,
125
+ pageSize: 20,
143
126
  });
144
127
  if (selected.type === 'create_new') {
145
128
  return handleCreateNewTask(api);
@@ -190,7 +173,7 @@ async function handleCreateNewTask(api) {
190
173
  });
191
174
  console.log(pc.dim('\nCreating task in Damper...'));
192
175
  const task = await api.createTask(title.trim(), type);
193
- console.log(pc.green(`✓ Created task #${task.id}: ${task.title}`));
176
+ console.log(pc.green(`✓ Created task #${shortIdRaw(task.id)}: ${task.title}`));
194
177
  return {
195
178
  task,
196
179
  isResume: false,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@damper/cli",
3
- "version": "0.6.10",
3
+ "version": "0.6.12",
4
4
  "description": "CLI tool for orchestrating Damper task workflows with Claude Code",
5
5
  "author": "Damper <hello@usedamper.com>",
6
6
  "repository": {