claude-code-kanban 1.9.0 → 1.10.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/README.md +24 -27
- package/package.json +2 -2
- package/public/index.html +105 -30
- package/server.js +20 -4
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Claude
|
|
1
|
+
# Claude Code Kanban
|
|
2
2
|
|
|
3
3
|
A real-time Kanban board for **observing** Claude Code tasks. See what Claude is working on, track dependencies between tasks, and manage task cleanup and priority.
|
|
4
4
|
|
|
@@ -8,7 +8,7 @@ A real-time Kanban board for **observing** Claude Code tasks. See what Claude is
|
|
|
8
8
|
|
|
9
9
|
## Why Use This?
|
|
10
10
|
|
|
11
|
-
When Claude Code breaks down complex work into tasks, you get visibility into its thinking — but only in the terminal. Claude
|
|
11
|
+
When Claude Code breaks down complex work into tasks, you get visibility into its thinking — but only in the terminal. Claude Code Kanban gives you a persistent, visual dashboard to:
|
|
12
12
|
|
|
13
13
|
- **See the big picture** — All your sessions and tasks in one place
|
|
14
14
|
- **Know what's happening now** — Live Updates show exactly what Claude is doing across all sessions
|
|
@@ -24,6 +24,11 @@ Claude Code controls task state — the viewer shows you what's happening:
|
|
|
24
24
|
- **Task dependencies** — Visualise blockedBy/blocks relationships to understand the critical path
|
|
25
25
|
- **Live activity feed** — Real-time stream of all in-progress tasks across every session
|
|
26
26
|
|
|
27
|
+
### Agent Teams Support
|
|
28
|
+
- **Team detection** — Automatically detects team sessions with multiple agents
|
|
29
|
+
- **Owner filtering** — Filter Kanban board by team member with color-coded agent indicators
|
|
30
|
+
- **Member count badges** — See how many agents are working in each session
|
|
31
|
+
|
|
27
32
|
### Cleanup Operations
|
|
28
33
|
- **Delete tasks** — Remove tasks with the delete button or press `D` (includes safety checks for dependencies)
|
|
29
34
|
- **Bulk delete** — Delete all tasks in a session at once
|
|
@@ -32,8 +37,9 @@ Claude Code controls task state — the viewer shows you what's happening:
|
|
|
32
37
|
View and organize your Claude Code sessions:
|
|
33
38
|
- **Session discovery** — Automatically finds all sessions in `~/.claude/tasks/` and `~/.claude/projects/`
|
|
34
39
|
- **View project paths** — See the full filesystem path for each project
|
|
40
|
+
- **Git branch display** — See which branch each session is working on
|
|
35
41
|
- **Fuzzy search** — Search across session names, task descriptions, and project paths with instant filtering
|
|
36
|
-
- **Session
|
|
42
|
+
- **Session filters** — Filter by active/all sessions and by project
|
|
37
43
|
|
|
38
44
|
### Keyboard Shortcuts
|
|
39
45
|
- `?` — Show help with all keyboard shortcuts
|
|
@@ -45,15 +51,22 @@ View and organize your Claude Code sessions:
|
|
|
45
51
|
### Quick start
|
|
46
52
|
|
|
47
53
|
```bash
|
|
48
|
-
npx claude-
|
|
54
|
+
npx claude-code-kanban
|
|
49
55
|
```
|
|
50
56
|
|
|
51
57
|
Open http://localhost:3456
|
|
52
58
|
|
|
59
|
+
### Global install
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
npm install -g claude-code-kanban
|
|
63
|
+
claude-code-kanban --open
|
|
64
|
+
```
|
|
65
|
+
|
|
53
66
|
### From source
|
|
54
67
|
|
|
55
68
|
```bash
|
|
56
|
-
git clone https://github.com/
|
|
69
|
+
git clone https://github.com/NikiforovAll/claude-task-viewer.git
|
|
57
70
|
cd claude-task-viewer
|
|
58
71
|
npm install
|
|
59
72
|
npm start
|
|
@@ -73,6 +86,8 @@ Claude Code stores tasks in `~/.claude/tasks/`. Each session has its own folder:
|
|
|
73
86
|
|
|
74
87
|
The viewer watches this directory and pushes updates via Server-Sent Events. Changes appear instantly — no polling, no refresh needed.
|
|
75
88
|
|
|
89
|
+
If port 3456 is already in use, the server automatically falls back to a random available port.
|
|
90
|
+
|
|
76
91
|
## Task Structure
|
|
77
92
|
|
|
78
93
|
```json
|
|
@@ -94,13 +109,13 @@ The viewer watches this directory and pushes updates via Server-Sent Events. Cha
|
|
|
94
109
|
|
|
95
110
|
```bash
|
|
96
111
|
# Custom port
|
|
97
|
-
PORT=8080 npx claude-
|
|
112
|
+
PORT=8080 npx claude-code-kanban
|
|
98
113
|
|
|
99
114
|
# Open browser automatically
|
|
100
|
-
npx claude-
|
|
115
|
+
npx claude-code-kanban --open
|
|
101
116
|
|
|
102
117
|
# Use a different Claude config directory (for multiple accounts)
|
|
103
|
-
npx claude-
|
|
118
|
+
npx claude-code-kanban --dir=~/.claude-work
|
|
104
119
|
```
|
|
105
120
|
|
|
106
121
|
## API
|
|
@@ -112,6 +127,7 @@ npx claude-task-viewer --dir=~/.claude-work
|
|
|
112
127
|
| `/api/tasks/all` | GET | Get all tasks across all sessions |
|
|
113
128
|
| `/api/tasks/:session/:task` | DELETE | Delete a task (checks dependencies) |
|
|
114
129
|
| `/api/tasks/:session/:task/note` | POST | Add a note to a task |
|
|
130
|
+
| `/api/teams/:name` | GET | Load team configuration |
|
|
115
131
|
| `/api/events` | GET | SSE stream for live updates |
|
|
116
132
|
|
|
117
133
|
## Design Philosophy
|
|
@@ -120,25 +136,6 @@ npx claude-task-viewer --dir=~/.claude-work
|
|
|
120
136
|
|
|
121
137
|
**Limited interaction:** You can delete tasks and add notes, but task status, subject, and description reflect Claude's actual work and can only be changed by Claude Code itself.
|
|
122
138
|
|
|
123
|
-
## Roadmap
|
|
124
|
-
|
|
125
|
-
### ✅ Completed
|
|
126
|
-
- **Real-time observation** — Live updates feed showing what Claude is doing across all sessions
|
|
127
|
-
- **Task dependencies** — Visualise blockedBy/blocks relationships
|
|
128
|
-
- **Task deletion** — Delete tasks with dependency checking
|
|
129
|
-
- **Keyboard shortcuts** — ?, D, Esc for quick actions
|
|
130
|
-
- **Session discovery** — Automatic detection of all Claude Code sessions
|
|
131
|
-
- **Search** — Search across sessions and tasks
|
|
132
|
-
|
|
133
|
-
### 🚧 Planned
|
|
134
|
-
- **Enhanced search & filter** — Filter by status, dependencies, date ranges
|
|
135
|
-
- **Session grouping** — Group sessions by project or time period
|
|
136
|
-
- **Task timeline** — See when tasks were created and completed
|
|
137
|
-
- **Export** — Export session data for analysis or reporting
|
|
138
|
-
- **Desktop notifications** — Optional notifications when tasks complete
|
|
139
|
-
|
|
140
|
-
[Open an issue](https://github.com/L1AD/claude-task-viewer/issues) with ideas or feedback.
|
|
141
|
-
|
|
142
139
|
## License
|
|
143
140
|
|
|
144
141
|
MIT
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-code-kanban",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.10.0",
|
|
4
4
|
"description": "A web-based Kanban board for viewing Claude Code tasks with agent teams support",
|
|
5
5
|
"main": "server.js",
|
|
6
6
|
"bin": {
|
|
7
|
-
"claude-
|
|
7
|
+
"claude-code-kanban": "./server.js"
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
10
|
"start": "node server.js",
|
package/public/index.html
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
6
|
<title>CC Kanban</title>
|
|
7
|
+
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='6' fill='%231a1a1a'/%3E%3Crect x='4' y='6' width='7' height='20' rx='2' fill='%23e8927c'/%3E%3Crect x='13' y='6' width='7' height='14' rx='2' fill='%23e8927c'/%3E%3Crect x='22' y='6' width='7' height='8' rx='2' fill='%23e8927c'/%3E%3C/svg%3E">
|
|
7
8
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
8
9
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
9
10
|
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600&family=Playfair+Display:wght@400;500;600&display=swap" rel="stylesheet">
|
|
@@ -173,6 +174,12 @@
|
|
|
173
174
|
padding: 0 16px 10px;
|
|
174
175
|
}
|
|
175
176
|
|
|
177
|
+
.reset-btn {
|
|
178
|
+
flex-shrink: 0;
|
|
179
|
+
width: 32px;
|
|
180
|
+
height: 32px;
|
|
181
|
+
}
|
|
182
|
+
|
|
176
183
|
.filter-dropdown {
|
|
177
184
|
flex: 1;
|
|
178
185
|
appearance: none;
|
|
@@ -340,31 +347,14 @@
|
|
|
340
347
|
|
|
341
348
|
.session-branch {
|
|
342
349
|
font-size: 10px;
|
|
343
|
-
color: var(--
|
|
344
|
-
margin-top:
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
border-radius: 3px;
|
|
348
|
-
display: inline-block;
|
|
349
|
-
font-family: var(--mono);
|
|
350
|
-
white-space: nowrap;
|
|
351
|
-
overflow: hidden;
|
|
352
|
-
text-overflow: ellipsis;
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
.session-branch {
|
|
356
|
-
font-size: 10px;
|
|
357
|
-
color: var(--accent);
|
|
358
|
-
margin-top: 3px;
|
|
359
|
-
padding: 2px 6px;
|
|
360
|
-
background: var(--border);
|
|
361
|
-
border-radius: 3px;
|
|
362
|
-
display: inline-block;
|
|
350
|
+
color: var(--text-muted);
|
|
351
|
+
margin-top: 2px;
|
|
352
|
+
display: block;
|
|
353
|
+
font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Fira Code', monospace;
|
|
363
354
|
white-space: nowrap;
|
|
364
355
|
overflow: hidden;
|
|
365
356
|
text-overflow: ellipsis;
|
|
366
|
-
|
|
367
|
-
font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Fira Code', monospace;
|
|
357
|
+
opacity: 0.7;
|
|
368
358
|
}
|
|
369
359
|
|
|
370
360
|
.session-progress {
|
|
@@ -1650,6 +1640,12 @@
|
|
|
1650
1640
|
<option value="50">Show 50</option>
|
|
1651
1641
|
<option value="all">Show All</option>
|
|
1652
1642
|
</select>
|
|
1643
|
+
<button class="icon-btn reset-btn" onclick="resetState()" title="Reset all filters" aria-label="Reset all filters">
|
|
1644
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16">
|
|
1645
|
+
<path d="M3 12a9 9 0 1 1 9 9 9.75 9.75 0 0 1-6.74-2.74L3 16"/>
|
|
1646
|
+
<path d="M3 22v-6h6"/>
|
|
1647
|
+
</svg>
|
|
1648
|
+
</button>
|
|
1653
1649
|
</div>
|
|
1654
1650
|
<div id="sessions-list" class="sessions-list"></div>
|
|
1655
1651
|
</div>
|
|
@@ -1758,14 +1754,57 @@
|
|
|
1758
1754
|
let currentSessionId = null;
|
|
1759
1755
|
let currentTasks = [];
|
|
1760
1756
|
let viewMode = 'session';
|
|
1761
|
-
let sessionFilter =
|
|
1762
|
-
let sessionLimit =
|
|
1757
|
+
let sessionFilter = 'active';
|
|
1758
|
+
let sessionLimit = '20';
|
|
1763
1759
|
let filterProject = null; // null = all projects, or project path to filter
|
|
1764
1760
|
let searchQuery = ''; // Search query for fuzzy search
|
|
1765
1761
|
let allTasksCache = []; // Cache all tasks for search
|
|
1766
1762
|
let bulkDeleteSessionId = null; // Track session for bulk delete
|
|
1767
1763
|
let ownerFilter = '';
|
|
1768
1764
|
|
|
1765
|
+
function getUrlState() {
|
|
1766
|
+
const params = new URLSearchParams(window.location.search);
|
|
1767
|
+
return {
|
|
1768
|
+
session: params.get('session'),
|
|
1769
|
+
view: params.get('view'),
|
|
1770
|
+
filter: params.get('filter'),
|
|
1771
|
+
limit: params.get('limit'),
|
|
1772
|
+
project: params.get('project'),
|
|
1773
|
+
owner: params.get('owner'),
|
|
1774
|
+
search: params.get('search'),
|
|
1775
|
+
};
|
|
1776
|
+
}
|
|
1777
|
+
|
|
1778
|
+
function updateUrl() {
|
|
1779
|
+
const params = new URLSearchParams();
|
|
1780
|
+
if (viewMode === 'all') params.set('view', 'all');
|
|
1781
|
+
if (currentSessionId) params.set('session', currentSessionId);
|
|
1782
|
+
if (sessionFilter !== 'active') params.set('filter', sessionFilter);
|
|
1783
|
+
if (sessionLimit !== '20') params.set('limit', sessionLimit);
|
|
1784
|
+
if (filterProject) params.set('project', filterProject);
|
|
1785
|
+
if (ownerFilter) params.set('owner', ownerFilter);
|
|
1786
|
+
if (searchQuery) params.set('search', searchQuery);
|
|
1787
|
+
const qs = params.toString();
|
|
1788
|
+
const url = qs ? `?${qs}` : window.location.pathname;
|
|
1789
|
+
history.replaceState(null, '', url);
|
|
1790
|
+
}
|
|
1791
|
+
|
|
1792
|
+
function resetState() {
|
|
1793
|
+
history.replaceState(null, '', window.location.pathname);
|
|
1794
|
+
sessionFilter = 'active';
|
|
1795
|
+
sessionLimit = '20';
|
|
1796
|
+
filterProject = null;
|
|
1797
|
+
ownerFilter = '';
|
|
1798
|
+
searchQuery = '';
|
|
1799
|
+
viewMode = 'all';
|
|
1800
|
+
currentSessionId = null;
|
|
1801
|
+
const searchInput = document.getElementById('search-input');
|
|
1802
|
+
if (searchInput) searchInput.value = '';
|
|
1803
|
+
document.getElementById('search-clear-btn')?.classList.remove('visible');
|
|
1804
|
+
loadPreferences();
|
|
1805
|
+
fetchSessions().then(() => showAllTasks());
|
|
1806
|
+
}
|
|
1807
|
+
|
|
1769
1808
|
// DOM
|
|
1770
1809
|
const sessionsList = document.getElementById('sessions-list');
|
|
1771
1810
|
const noSession = document.getElementById('no-session');
|
|
@@ -1826,6 +1865,7 @@
|
|
|
1826
1865
|
clearBtn.classList.remove('visible');
|
|
1827
1866
|
}
|
|
1828
1867
|
|
|
1868
|
+
updateUrl();
|
|
1829
1869
|
renderSessions();
|
|
1830
1870
|
}
|
|
1831
1871
|
|
|
@@ -1834,6 +1874,7 @@
|
|
|
1834
1874
|
searchInput.value = '';
|
|
1835
1875
|
searchQuery = '';
|
|
1836
1876
|
document.getElementById('search-clear-btn').classList.remove('visible');
|
|
1877
|
+
updateUrl();
|
|
1837
1878
|
renderSessions();
|
|
1838
1879
|
}
|
|
1839
1880
|
|
|
@@ -2090,12 +2131,14 @@
|
|
|
2090
2131
|
currentTasks = newTasks;
|
|
2091
2132
|
currentSessionId = sessionId;
|
|
2092
2133
|
ownerFilter = '';
|
|
2134
|
+
updateUrl();
|
|
2093
2135
|
renderSession();
|
|
2094
2136
|
} catch (error) {
|
|
2095
2137
|
console.error('Failed to fetch tasks:', error);
|
|
2096
2138
|
currentTasks = [];
|
|
2097
2139
|
currentSessionId = sessionId;
|
|
2098
2140
|
lastCurrentTasksHash = '';
|
|
2141
|
+
updateUrl();
|
|
2099
2142
|
renderSession();
|
|
2100
2143
|
}
|
|
2101
2144
|
}
|
|
@@ -2111,6 +2154,7 @@
|
|
|
2111
2154
|
tasks = tasks.filter(t => t.project === filterProject);
|
|
2112
2155
|
}
|
|
2113
2156
|
currentTasks = tasks;
|
|
2157
|
+
updateUrl();
|
|
2114
2158
|
renderAllTasks();
|
|
2115
2159
|
renderSessions();
|
|
2116
2160
|
} catch (error) {
|
|
@@ -2709,12 +2753,13 @@
|
|
|
2709
2753
|
let refreshTimer = null;
|
|
2710
2754
|
function debouncedRefresh(sessionId, isMetadata) {
|
|
2711
2755
|
clearTimeout(refreshTimer);
|
|
2756
|
+
const delay = isMetadata ? 2000 : 500;
|
|
2712
2757
|
refreshTimer = setTimeout(() => {
|
|
2713
2758
|
fetchSessions().catch(err => console.error('[SSE] fetchSessions failed:', err));
|
|
2714
2759
|
if (currentSessionId && (isMetadata || sessionId === currentSessionId)) {
|
|
2715
2760
|
fetchTasks(currentSessionId);
|
|
2716
2761
|
}
|
|
2717
|
-
},
|
|
2762
|
+
}, delay);
|
|
2718
2763
|
}
|
|
2719
2764
|
|
|
2720
2765
|
eventSource.onmessage = (event) => {
|
|
@@ -2778,18 +2823,19 @@
|
|
|
2778
2823
|
|
|
2779
2824
|
function filterBySessions(value) {
|
|
2780
2825
|
sessionFilter = value;
|
|
2781
|
-
|
|
2826
|
+
updateUrl();
|
|
2782
2827
|
renderSessions();
|
|
2783
2828
|
}
|
|
2784
2829
|
|
|
2785
2830
|
function changeSessionLimit(value) {
|
|
2786
2831
|
sessionLimit = value;
|
|
2787
|
-
|
|
2832
|
+
updateUrl();
|
|
2788
2833
|
fetchSessions();
|
|
2789
2834
|
}
|
|
2790
2835
|
|
|
2791
2836
|
function filterByProject(project) {
|
|
2792
2837
|
filterProject = project || null;
|
|
2838
|
+
updateUrl();
|
|
2793
2839
|
renderSessions();
|
|
2794
2840
|
fetchLiveUpdates();
|
|
2795
2841
|
showAllTasks();
|
|
@@ -2956,23 +3002,52 @@
|
|
|
2956
3002
|
const c = value ? getOwnerColor(value) : null;
|
|
2957
3003
|
select.style.color = c ? c.color : '';
|
|
2958
3004
|
select.style.backgroundColor = c ? c.bg : '';
|
|
3005
|
+
updateUrl();
|
|
2959
3006
|
renderKanban();
|
|
2960
3007
|
}
|
|
2961
3008
|
|
|
2962
3009
|
// Init
|
|
2963
3010
|
loadTheme();
|
|
3011
|
+
|
|
3012
|
+
const urlState = getUrlState();
|
|
3013
|
+
sessionFilter = urlState.filter || 'active';
|
|
3014
|
+
sessionLimit = urlState.limit || '20';
|
|
3015
|
+
filterProject = urlState.project || null;
|
|
3016
|
+
ownerFilter = urlState.owner || '';
|
|
3017
|
+
searchQuery = urlState.search || '';
|
|
3018
|
+
|
|
2964
3019
|
loadPreferences();
|
|
2965
3020
|
setupEventSource();
|
|
2966
3021
|
|
|
2967
|
-
|
|
3022
|
+
if (urlState.search) {
|
|
3023
|
+
document.getElementById('search-input').value = urlState.search;
|
|
3024
|
+
document.getElementById('search-clear-btn').classList.add('visible');
|
|
3025
|
+
}
|
|
3026
|
+
|
|
2968
3027
|
fetchSessions().then(() => {
|
|
2969
|
-
if (
|
|
2970
|
-
|
|
3028
|
+
if (urlState.view === 'all') {
|
|
3029
|
+
showAllTasks();
|
|
3030
|
+
} else if (urlState.session) {
|
|
3031
|
+
fetchTasks(urlState.session);
|
|
3032
|
+
} else if (sessions.length > 0) {
|
|
2971
3033
|
fetchTasks(sessions[0].id);
|
|
2972
3034
|
} else {
|
|
2973
3035
|
showAllTasks();
|
|
2974
3036
|
}
|
|
2975
3037
|
});
|
|
3038
|
+
|
|
3039
|
+
window.addEventListener('popstate', () => {
|
|
3040
|
+
const s = getUrlState();
|
|
3041
|
+
sessionFilter = s.filter || 'active';
|
|
3042
|
+
sessionLimit = s.limit || '20';
|
|
3043
|
+
filterProject = s.project || null;
|
|
3044
|
+
ownerFilter = s.owner || '';
|
|
3045
|
+
searchQuery = s.search || '';
|
|
3046
|
+
loadPreferences();
|
|
3047
|
+
if (s.view === 'all') showAllTasks();
|
|
3048
|
+
else if (s.session) fetchTasks(s.session);
|
|
3049
|
+
else if (sessions.length > 0) fetchTasks(sessions[0].id);
|
|
3050
|
+
});
|
|
2976
3051
|
</script>
|
|
2977
3052
|
|
|
2978
3053
|
<!-- Help Modal -->
|
package/server.js
CHANGED
|
@@ -517,11 +517,27 @@ projectsWatcher.on('all', (event, filePath) => {
|
|
|
517
517
|
});
|
|
518
518
|
|
|
519
519
|
// Start server
|
|
520
|
-
app.listen(PORT, () => {
|
|
521
|
-
|
|
520
|
+
const server = app.listen(PORT, () => {
|
|
521
|
+
const actualPort = server.address().port;
|
|
522
|
+
console.log(`Claude Task Viewer running at http://localhost:${actualPort}`);
|
|
522
523
|
|
|
523
|
-
// Open browser if --open flag is passed
|
|
524
524
|
if (process.argv.includes('--open')) {
|
|
525
|
-
import('open').then(open => open.default(`http://localhost:${
|
|
525
|
+
import('open').then(open => open.default(`http://localhost:${actualPort}`));
|
|
526
|
+
}
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
server.on('error', (err) => {
|
|
530
|
+
if (err.code === 'EADDRINUSE') {
|
|
531
|
+
console.log(`Port ${PORT} in use, trying random port...`);
|
|
532
|
+
const fallback = app.listen(0, () => {
|
|
533
|
+
const actualPort = fallback.address().port;
|
|
534
|
+
console.log(`Claude Task Viewer running at http://localhost:${actualPort}`);
|
|
535
|
+
|
|
536
|
+
if (process.argv.includes('--open')) {
|
|
537
|
+
import('open').then(open => open.default(`http://localhost:${actualPort}`));
|
|
538
|
+
}
|
|
539
|
+
});
|
|
540
|
+
} else {
|
|
541
|
+
throw err;
|
|
526
542
|
}
|
|
527
543
|
});
|