@ycniuqton/devlens 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.
Files changed (61) hide show
  1. package/README.md +164 -0
  2. package/bin/devlens.js +2 -0
  3. package/dist/index.d.ts +1 -0
  4. package/dist/index.js +205 -0
  5. package/dist/index.js.map +1 -0
  6. package/dist/init.d.ts +3 -0
  7. package/dist/init.js +239 -0
  8. package/dist/init.js.map +1 -0
  9. package/dist/routes/diff.d.ts +1 -0
  10. package/dist/routes/diff.js +39 -0
  11. package/dist/routes/diff.js.map +1 -0
  12. package/dist/routes/integrations.d.ts +1 -0
  13. package/dist/routes/integrations.js +132 -0
  14. package/dist/routes/integrations.js.map +1 -0
  15. package/dist/routes/rules.d.ts +1 -0
  16. package/dist/routes/rules.js +115 -0
  17. package/dist/routes/rules.js.map +1 -0
  18. package/dist/routes/tasks.d.ts +4 -0
  19. package/dist/routes/tasks.js +360 -0
  20. package/dist/routes/tasks.js.map +1 -0
  21. package/dist/server.d.ts +7 -0
  22. package/dist/server.js +112 -0
  23. package/dist/server.js.map +1 -0
  24. package/dist/services/claudeTasks.d.ts +23 -0
  25. package/dist/services/claudeTasks.js +160 -0
  26. package/dist/services/claudeTasks.js.map +1 -0
  27. package/dist/services/config.d.ts +3 -0
  28. package/dist/services/config.js +25 -0
  29. package/dist/services/config.js.map +1 -0
  30. package/dist/services/git.d.ts +8 -0
  31. package/dist/services/git.js +90 -0
  32. package/dist/services/git.js.map +1 -0
  33. package/dist/services/jira.d.ts +11 -0
  34. package/dist/services/jira.js +52 -0
  35. package/dist/services/jira.js.map +1 -0
  36. package/dist/services/linear.d.ts +9 -0
  37. package/dist/services/linear.js +69 -0
  38. package/dist/services/linear.js.map +1 -0
  39. package/dist/services/rules.d.ts +14 -0
  40. package/dist/services/rules.js +133 -0
  41. package/dist/services/rules.js.map +1 -0
  42. package/dist/services/taskStore.d.ts +27 -0
  43. package/dist/services/taskStore.js +261 -0
  44. package/dist/services/taskStore.js.map +1 -0
  45. package/dist/services/tunnel.d.ts +8 -0
  46. package/dist/services/tunnel.js +152 -0
  47. package/dist/services/tunnel.js.map +1 -0
  48. package/dist/services/watcher.d.ts +2 -0
  49. package/dist/services/watcher.js +30 -0
  50. package/dist/services/watcher.js.map +1 -0
  51. package/dist/types/index.d.ts +87 -0
  52. package/dist/types/index.js +3 -0
  53. package/dist/types/index.js.map +1 -0
  54. package/package.json +53 -0
  55. package/public/css/style.css +1613 -0
  56. package/public/index.html +395 -0
  57. package/public/js/app.js +104 -0
  58. package/public/js/diff.js +337 -0
  59. package/public/js/integrations.js +194 -0
  60. package/public/js/rules.js +174 -0
  61. package/public/js/tasks.js +301 -0
@@ -0,0 +1,174 @@
1
+ // Rules tab — manage .devlens/rules.md
2
+
3
+ let currentRules = [];
4
+
5
+ async function loadRules() {
6
+ try {
7
+ const res = await fetch('/api/rules');
8
+ currentRules = await res.json();
9
+ renderRules();
10
+ } catch (err) {
11
+ showToast('Failed to load rules', 'error');
12
+ }
13
+ }
14
+
15
+ function renderRules() {
16
+ const list = document.getElementById('rules-list');
17
+ if (!list) return;
18
+
19
+ if (!currentRules.length) {
20
+ list.innerHTML = '<p class="panel-empty">No rules defined</p>';
21
+ return;
22
+ }
23
+
24
+ // Sort: protected (default) first
25
+ const sorted = [...currentRules].sort((a, b) => (b.protected ? 1 : 0) - (a.protected ? 1 : 0));
26
+
27
+ list.innerHTML = sorted.map(rule => {
28
+ const lockIcon = `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>`;
29
+ const trashIcon = `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>`;
30
+
31
+ return `
32
+ <div class="rule-card ${rule.active ? 'active' : 'inactive'} ${rule.protected ? 'protected' : ''}">
33
+ <label class="rule-toggle">
34
+ <input type="checkbox" ${rule.active ? 'checked' : ''} onchange="toggleRule(${rule.index})">
35
+ <span class="rule-toggle-slider"></span>
36
+ </label>
37
+ <div class="rule-content">
38
+ <div class="rule-text">${escapeHtml(rule.content)}</div>
39
+ ${rule.protected ? '<span class="rule-default-badge">DEFAULT</span>' : ''}
40
+ </div>
41
+ <div class="rule-actions">
42
+ ${rule.protected
43
+ ? `<span class="rule-lock" title="Protected — cannot be deleted">${lockIcon}</span>`
44
+ : `<button class="btn-icon btn-icon-sm rule-delete" onclick="deleteRule(${rule.index})" title="Delete rule">${trashIcon}</button>`}
45
+ </div>
46
+ </div>
47
+ `;
48
+ }).join('');
49
+ }
50
+
51
+ async function toggleRule(index) {
52
+ try {
53
+ await fetch(`/api/rules/${index}/toggle`, { method: 'PATCH' });
54
+ showToast('Rule toggled', 'success');
55
+ loadRules();
56
+ } catch {
57
+ showToast('Failed to toggle rule', 'error');
58
+ }
59
+ }
60
+
61
+ async function deleteRule(index) {
62
+ if (!confirm('Delete this rule?')) return;
63
+ try {
64
+ const res = await fetch(`/api/rules/${index}`, { method: 'DELETE' });
65
+ if (!res.ok) {
66
+ const err = await res.json();
67
+ showToast(err.error || 'Cannot delete', 'error');
68
+ return;
69
+ }
70
+ showToast('Rule deleted', 'success');
71
+ loadRules();
72
+ } catch {
73
+ showToast('Failed to delete rule', 'error');
74
+ }
75
+ }
76
+
77
+ async function addRule(content) {
78
+ if (!content || !content.trim()) return;
79
+ try {
80
+ await fetch('/api/rules', {
81
+ method: 'POST',
82
+ headers: { 'Content-Type': 'application/json' },
83
+ body: JSON.stringify({ content: content.trim() }),
84
+ });
85
+ showToast('Rule added', 'success');
86
+ loadRules();
87
+ } catch {
88
+ showToast('Failed to add rule', 'error');
89
+ }
90
+ }
91
+
92
+ // Add Rule button → simple prompt
93
+ document.getElementById('add-rule-btn')?.addEventListener('click', () => {
94
+ const content = prompt('Enter the new rule:');
95
+ if (content) addRule(content);
96
+ });
97
+
98
+ // Preset buttons
99
+ document.querySelectorAll('.preset-btn').forEach(btn => {
100
+ btn.addEventListener('click', () => {
101
+ const preset = btn.getAttribute('data-preset');
102
+ if (preset) addRule(preset);
103
+ });
104
+ });
105
+
106
+ // ============================================================
107
+ // Commit Approval
108
+ // ============================================================
109
+ async function loadCommitApproval() {
110
+ try {
111
+ const res = await fetch('/api/rules/commit-approval/status');
112
+ const data = await res.json();
113
+ renderCommitApproval(data);
114
+ } catch {}
115
+ }
116
+
117
+ function renderCommitApproval(data) {
118
+ const pendingBanner = document.getElementById('commit-approval-banner');
119
+ const approvedBanner = document.getElementById('commit-approved-banner');
120
+ if (!pendingBanner || !approvedBanner) return;
121
+
122
+ if (data.pending) {
123
+ pendingBanner.style.display = '';
124
+ document.getElementById('commit-approval-message').textContent = data.pending;
125
+ approvedBanner.style.display = 'none';
126
+ } else {
127
+ pendingBanner.style.display = 'none';
128
+ }
129
+
130
+ if (data.approved && data.approvedAt) {
131
+ approvedBanner.style.display = '';
132
+ document.getElementById('commit-approved-time').textContent = new Date(data.approvedAt).toLocaleString();
133
+ } else if (!data.pending) {
134
+ approvedBanner.style.display = 'none';
135
+ }
136
+ }
137
+
138
+ document.getElementById('approve-commit-btn')?.addEventListener('click', async () => {
139
+ try {
140
+ await fetch('/api/rules/commit-approval/approve', { method: 'POST' });
141
+ showToast('Commit approved', 'success');
142
+ loadCommitApproval();
143
+ } catch {
144
+ showToast('Failed to approve', 'error');
145
+ }
146
+ });
147
+
148
+ document.getElementById('reject-commit-btn')?.addEventListener('click', async () => {
149
+ try {
150
+ await fetch('/api/rules/commit-approval/reject', { method: 'POST' });
151
+ showToast('Commit rejected', 'info');
152
+ loadCommitApproval();
153
+ } catch {
154
+ showToast('Failed to reject', 'error');
155
+ }
156
+ });
157
+
158
+ // WebSocket handlers
159
+ function handleRulesUpdate(payload) {
160
+ if (payload?.rules) {
161
+ currentRules = payload.rules;
162
+ renderRules();
163
+ } else {
164
+ loadRules();
165
+ }
166
+ }
167
+
168
+ function handleCommitApprovalUpdate(payload) {
169
+ if (payload) renderCommitApproval(payload);
170
+ }
171
+
172
+ // Initial load
173
+ loadRules();
174
+ loadCommitApproval();
@@ -0,0 +1,301 @@
1
+ // Task board
2
+ const modal = document.getElementById('task-modal');
3
+ const taskForm = document.getElementById('task-form');
4
+ let allTasks = [];
5
+ let currentSessionFilter = '';
6
+
7
+ document.getElementById('add-task-btn').addEventListener('click', () => openModal());
8
+
9
+ // Session filter dropdown
10
+ const sessionSelect = document.getElementById('session-filter');
11
+ sessionSelect?.addEventListener('change', () => {
12
+ currentSessionFilter = sessionSelect.value;
13
+ loadTasks();
14
+ });
15
+ document.addEventListener('keydown', (e) => {
16
+ if (e.key === 'Escape' && !modal.classList.contains('hidden')) closeModal();
17
+ });
18
+ document.getElementById('modal-cancel').addEventListener('click', () => closeModal());
19
+ document.getElementById('modal-close').addEventListener('click', () => closeModal());
20
+ document.querySelector('.modal-backdrop').addEventListener('click', () => closeModal());
21
+
22
+ taskForm.addEventListener('submit', async (e) => {
23
+ e.preventDefault();
24
+ const id = document.getElementById('task-id').value;
25
+ const body = {
26
+ title: document.getElementById('task-title-input').value,
27
+ description: document.getElementById('task-desc-input').value,
28
+ priority: document.getElementById('task-priority-input').value,
29
+ status: document.getElementById('task-status-input').value,
30
+ tags: document.getElementById('task-tags-input').value
31
+ .split(',').map(t => t.trim()).filter(Boolean),
32
+ };
33
+
34
+ try {
35
+ if (id) {
36
+ await fetch(`/api/tasks/${id}`, {
37
+ method: 'PUT',
38
+ headers: { 'Content-Type': 'application/json' },
39
+ body: JSON.stringify(body),
40
+ });
41
+ } else {
42
+ await fetch('/api/tasks', {
43
+ method: 'POST',
44
+ headers: { 'Content-Type': 'application/json' },
45
+ body: JSON.stringify(body),
46
+ });
47
+ }
48
+ closeModal();
49
+ loadTasks();
50
+ } catch (err) {
51
+ showToast('Failed to save task', 'error');
52
+ }
53
+ });
54
+
55
+ function openModal(task = null) {
56
+ document.getElementById('modal-title').textContent = task ? 'Edit Task' : 'New Task';
57
+ document.getElementById('task-id').value = task ? task.id : '';
58
+ document.getElementById('task-title-input').value = task ? task.title : '';
59
+ document.getElementById('task-desc-input').value = task ? task.description : '';
60
+ document.getElementById('task-priority-input').value = task ? task.priority : 'medium';
61
+ document.getElementById('task-status-input').value = task ? task.status : 'pending';
62
+ document.getElementById('task-tags-input').value = task ? task.tags.join(', ') : '';
63
+
64
+ // Header: task number + status badge
65
+ const taskNum = document.getElementById('modal-task-number');
66
+ const statusBadge = document.getElementById('modal-status-badge');
67
+ if (task?.claudeTaskId) {
68
+ taskNum.textContent = `#${task.claudeTaskId}`;
69
+ } else {
70
+ taskNum.textContent = '';
71
+ }
72
+ if (task) {
73
+ statusBadge.textContent = task.status;
74
+ statusBadge.className = 'modal-status-badge ' + task.status;
75
+ } else {
76
+ statusBadge.textContent = '';
77
+ statusBadge.className = 'modal-status-badge';
78
+ }
79
+
80
+ // Footer: session hint
81
+ const sessionHint = document.getElementById('modal-session-hint');
82
+ sessionHint.textContent = task?.claudeSessionId ? `Session ${task.claudeSessionId.substring(0, 8)}` : '';
83
+
84
+ // Parse context (stored as JSON string with userPrompt, claudeReasoning, filesTouched)
85
+ let ctx = null;
86
+ if (task?.context) {
87
+ try { ctx = typeof task.context === 'string' ? JSON.parse(task.context) : task.context; } catch { ctx = { userPrompt: task.context }; }
88
+ }
89
+
90
+ // User prompt
91
+ const contextGroup = document.getElementById('task-context-group');
92
+ if (ctx?.userPrompt) {
93
+ document.getElementById('task-context-prompt').textContent = ctx.userPrompt;
94
+ contextGroup.style.display = '';
95
+ } else {
96
+ contextGroup.style.display = 'none';
97
+ }
98
+
99
+ // Claude reasoning
100
+ const reasoningGroup = document.getElementById('task-reasoning-group');
101
+ if (ctx?.claudeReasoning) {
102
+ document.getElementById('task-context-reasoning').textContent = ctx.claudeReasoning;
103
+ reasoningGroup.style.display = '';
104
+ } else {
105
+ reasoningGroup.style.display = 'none';
106
+ }
107
+
108
+ // Files touched
109
+ const filesGroup = document.getElementById('task-files-group');
110
+ if (ctx?.filesTouched?.length) {
111
+ document.getElementById('task-context-files').textContent = ctx.filesTouched.join('\n');
112
+ filesGroup.style.display = '';
113
+ } else {
114
+ filesGroup.style.display = 'none';
115
+ }
116
+
117
+ // Completion context
118
+ let compCtx = null;
119
+ if (task?.completionContext) {
120
+ try { compCtx = typeof task.completionContext === 'string' ? JSON.parse(task.completionContext) : task.completionContext; } catch {}
121
+ }
122
+
123
+ const completionGroup = document.getElementById('task-completion-group');
124
+ if (compCtx?.claudeReasoning) {
125
+ document.getElementById('task-completion-display').textContent = compCtx.claudeReasoning;
126
+ completionGroup.style.display = '';
127
+ } else {
128
+ completionGroup.style.display = 'none';
129
+ }
130
+
131
+ const completionFilesGroup = document.getElementById('task-completion-files-group');
132
+ if (compCtx?.filesTouched?.length) {
133
+ document.getElementById('task-completion-files').textContent = compCtx.filesTouched.join('\n');
134
+ completionFilesGroup.style.display = '';
135
+ } else {
136
+ completionFilesGroup.style.display = 'none';
137
+ }
138
+
139
+ // Show Claude metadata if available
140
+ const metaGroup = document.getElementById('task-claude-meta-group');
141
+ const metaDisplay = document.getElementById('task-claude-meta');
142
+ const metaParts = [];
143
+ if (task?.claudeTaskId) metaParts.push(`Task ID: #${task.claudeTaskId}`);
144
+ if (task?.claudeSessionId) metaParts.push(`Session: ${task.claudeSessionId.substring(0, 8)}`);
145
+ if (task?.activeForm) metaParts.push(`Active: ${task.activeForm}`);
146
+ if (task?.owner) metaParts.push(`Owner: ${task.owner}`);
147
+ if (task?.completedAt) metaParts.push(`Completed: ${task.completedAt}`);
148
+ if (task?.metadata) metaParts.push(`Metadata: ${JSON.stringify(task.metadata)}`);
149
+
150
+ if (metaParts.length > 0) {
151
+ metaDisplay.textContent = metaParts.join('\n');
152
+ metaGroup.style.display = '';
153
+ } else {
154
+ metaGroup.style.display = 'none';
155
+ }
156
+
157
+ // Show/hide files row
158
+ const filesRow = document.getElementById('modal-row-files');
159
+ const hasFilesRow = ctx?.filesTouched?.length || compCtx?.filesTouched?.length || metaParts.length > 0;
160
+ if (filesRow) filesRow.style.display = hasFilesRow ? '' : 'none';
161
+
162
+ modal.classList.remove('hidden');
163
+ }
164
+
165
+ function closeModal() {
166
+ modal.classList.add('hidden');
167
+ taskForm.reset();
168
+ }
169
+
170
+ // ---- Kanban Board ----
171
+ async function loadTasks() {
172
+ try {
173
+ const params = currentSessionFilter ? `?session=${currentSessionFilter}` : '';
174
+ const res = await fetch(`/api/tasks${params}`);
175
+ allTasks = await res.json();
176
+ renderBoard();
177
+ } catch (err) {
178
+ showToast('Failed to load tasks', 'error');
179
+ }
180
+ }
181
+
182
+ async function loadSessions() {
183
+ try {
184
+ const res = await fetch('/api/tasks/sessions');
185
+ const sessions = await res.json();
186
+ renderSessionDropdown(sessions);
187
+ } catch {}
188
+ }
189
+
190
+ function renderSessionDropdown(sessions) {
191
+ const select = document.getElementById('session-filter');
192
+ if (!select) return;
193
+
194
+ const current = select.value;
195
+ select.innerHTML = '<option value="">All Sessions</option>';
196
+
197
+ for (const s of sessions) {
198
+ const name = s.name || s.sessionId.substring(0, 8);
199
+ const dot = s.status === 'active' ? '\u{1F7E2}' : '\u26AA';
200
+ const opt = document.createElement('option');
201
+ opt.value = s.sessionId;
202
+ opt.textContent = `${dot} ${name} (${s.taskCount})`;
203
+ select.appendChild(opt);
204
+ }
205
+
206
+ // Restore selection
207
+ if (current) select.value = current;
208
+ }
209
+
210
+ function renderBoard() {
211
+ ['pending', 'in-progress', 'completed', 'archived'].forEach(status => {
212
+ const column = document.querySelector(`.column-cards[data-status="${status}"]`);
213
+ const tasks = allTasks.filter(t => t.status === status);
214
+ const count = column.closest('.kanban-column').querySelector('.count');
215
+ count.textContent = tasks.length;
216
+
217
+ column.innerHTML = tasks.map(task => `
218
+ <div class="task-card priority-${task.priority}" draggable="true" data-id="${task.id}">
219
+ <div class="task-title">${escapeHtml(task.title)}</div>
220
+ ${task.description ? `<div style="font-size:12px;color:var(--color-text-secondary);margin-bottom:6px">${escapeHtml(task.description).substring(0, 100)}</div>` : ''}
221
+ <div class="task-meta">
222
+ ${task.tags.map(t => `<span class="tag">${escapeHtml(t)}</span>`).join('')}
223
+ </div>
224
+ ${task.dependencies.length ? `<div class="task-deps">Blocked by: ${task.dependencies.length} task(s)</div>` : ''}
225
+ <div class="card-actions">
226
+ <button onclick="editTask('${task.id}')">Edit</button>
227
+ <button class="delete" onclick="deleteTask('${task.id}')">Delete</button>
228
+ </div>
229
+ </div>
230
+ `).join('');
231
+
232
+ // Click to edit + drag events
233
+ column.querySelectorAll('.task-card').forEach(card => {
234
+ card.addEventListener('click', (e) => {
235
+ if (e.target.closest('.card-actions')) return; // don't trigger on Edit/Delete buttons
236
+ editTask(card.dataset.id);
237
+ });
238
+ card.addEventListener('dragstart', (e) => {
239
+ e.dataTransfer.setData('text/plain', card.dataset.id);
240
+ card.classList.add('dragging');
241
+ });
242
+ card.addEventListener('dragend', () => card.classList.remove('dragging'));
243
+ });
244
+ });
245
+
246
+ // Drop targets
247
+ document.querySelectorAll('.column-cards').forEach(col => {
248
+ col.addEventListener('dragover', (e) => {
249
+ e.preventDefault();
250
+ col.classList.add('drag-over');
251
+ });
252
+ col.addEventListener('dragleave', () => col.classList.remove('drag-over'));
253
+ col.addEventListener('drop', async (e) => {
254
+ e.preventDefault();
255
+ col.classList.remove('drag-over');
256
+ const taskId = e.dataTransfer.getData('text/plain');
257
+ const newStatus = col.dataset.status;
258
+ try {
259
+ await fetch(`/api/tasks/${taskId}`, {
260
+ method: 'PUT',
261
+ headers: { 'Content-Type': 'application/json' },
262
+ body: JSON.stringify({ status: newStatus }),
263
+ });
264
+ loadTasks();
265
+ } catch (err) {
266
+ showToast('Failed to update task', 'error');
267
+ }
268
+ });
269
+ });
270
+ }
271
+
272
+ async function editTask(id) {
273
+ const task = allTasks.find(t => t.id === id);
274
+ if (task) openModal(task);
275
+ }
276
+
277
+ async function deleteTask(id) {
278
+ if (!confirm('Delete this task?')) return;
279
+ try {
280
+ await fetch(`/api/tasks/${id}`, { method: 'DELETE' });
281
+ loadTasks();
282
+ } catch (err) {
283
+ showToast('Failed to delete task', 'error');
284
+ }
285
+ }
286
+
287
+ // ---- WebSocket handlers ----
288
+ function handleTaskUpdate() {
289
+ loadSessions();
290
+ loadTasks();
291
+ }
292
+
293
+ function escapeHtml(str) {
294
+ const div = document.createElement('div');
295
+ div.textContent = str;
296
+ return div.innerHTML;
297
+ }
298
+
299
+ // Initial load
300
+ loadSessions();
301
+ loadTasks();