tlc-claude-code 1.4.4 → 1.4.6

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 (72) hide show
  1. package/dashboard/dist/App.js +28 -2
  2. package/dashboard/dist/api/health-diagnostics.d.ts +26 -0
  3. package/dashboard/dist/api/health-diagnostics.js +85 -0
  4. package/dashboard/dist/api/health-diagnostics.test.d.ts +1 -0
  5. package/dashboard/dist/api/health-diagnostics.test.js +126 -0
  6. package/dashboard/dist/api/index.d.ts +5 -0
  7. package/dashboard/dist/api/index.js +5 -0
  8. package/dashboard/dist/api/notes-api.d.ts +18 -0
  9. package/dashboard/dist/api/notes-api.js +68 -0
  10. package/dashboard/dist/api/notes-api.test.d.ts +1 -0
  11. package/dashboard/dist/api/notes-api.test.js +113 -0
  12. package/dashboard/dist/api/safeFetch.d.ts +50 -0
  13. package/dashboard/dist/api/safeFetch.js +135 -0
  14. package/dashboard/dist/api/safeFetch.test.d.ts +1 -0
  15. package/dashboard/dist/api/safeFetch.test.js +215 -0
  16. package/dashboard/dist/api/tasks-api.d.ts +32 -0
  17. package/dashboard/dist/api/tasks-api.js +98 -0
  18. package/dashboard/dist/api/tasks-api.test.d.ts +1 -0
  19. package/dashboard/dist/api/tasks-api.test.js +383 -0
  20. package/dashboard/dist/components/BugsPane.d.ts +20 -0
  21. package/dashboard/dist/components/BugsPane.js +210 -0
  22. package/dashboard/dist/components/BugsPane.test.d.ts +1 -0
  23. package/dashboard/dist/components/BugsPane.test.js +256 -0
  24. package/dashboard/dist/components/HealthPane.d.ts +3 -1
  25. package/dashboard/dist/components/HealthPane.js +44 -6
  26. package/dashboard/dist/components/HealthPane.test.js +105 -2
  27. package/dashboard/dist/components/RouterPane.d.ts +4 -3
  28. package/dashboard/dist/components/RouterPane.js +60 -57
  29. package/dashboard/dist/components/RouterPane.test.js +150 -96
  30. package/dashboard/dist/components/UpdateBanner.d.ts +26 -0
  31. package/dashboard/dist/components/UpdateBanner.js +30 -0
  32. package/dashboard/dist/components/UpdateBanner.test.d.ts +1 -0
  33. package/dashboard/dist/components/UpdateBanner.test.js +96 -0
  34. package/dashboard/dist/components/ui/EmptyState.d.ts +14 -0
  35. package/dashboard/dist/components/ui/EmptyState.js +58 -0
  36. package/dashboard/dist/components/ui/EmptyState.test.d.ts +1 -0
  37. package/dashboard/dist/components/ui/EmptyState.test.js +97 -0
  38. package/dashboard/dist/components/ui/ErrorState.d.ts +17 -0
  39. package/dashboard/dist/components/ui/ErrorState.js +80 -0
  40. package/dashboard/dist/components/ui/ErrorState.test.d.ts +1 -0
  41. package/dashboard/dist/components/ui/ErrorState.test.js +166 -0
  42. package/dashboard/package.json +3 -0
  43. package/package.json +4 -1
  44. package/server/dashboard/index.html +284 -13
  45. package/server/dashboard/login.html +262 -0
  46. package/server/index.js +304 -0
  47. package/server/lib/api-provider.js +104 -186
  48. package/server/lib/api-provider.test.js +238 -336
  49. package/server/lib/cli-detector.js +90 -166
  50. package/server/lib/cli-detector.test.js +114 -269
  51. package/server/lib/cli-provider.js +142 -212
  52. package/server/lib/cli-provider.test.js +196 -349
  53. package/server/lib/debug.test.js +3 -3
  54. package/server/lib/devserver-router-api.js +54 -249
  55. package/server/lib/devserver-router-api.test.js +126 -426
  56. package/server/lib/introspect.js +309 -0
  57. package/server/lib/introspect.test.js +286 -0
  58. package/server/lib/model-router.js +107 -245
  59. package/server/lib/model-router.test.js +122 -313
  60. package/server/lib/output-schemas.js +146 -269
  61. package/server/lib/output-schemas.test.js +106 -307
  62. package/server/lib/plan-parser.js +59 -16
  63. package/server/lib/provider-interface.js +99 -153
  64. package/server/lib/provider-interface.test.js +228 -394
  65. package/server/lib/provider-queue.js +164 -158
  66. package/server/lib/provider-queue.test.js +186 -315
  67. package/server/lib/router-config.js +99 -221
  68. package/server/lib/router-config.test.js +83 -237
  69. package/server/lib/router-setup-command.js +94 -419
  70. package/server/lib/router-setup-command.test.js +96 -375
  71. package/server/lib/router-status-api.js +93 -0
  72. package/server/lib/router-status-api.test.js +270 -0
@@ -0,0 +1,135 @@
1
+ /**
2
+ * Self-healing error boundary utilities for fetch operations.
3
+ * Provides consistent error handling, timeout support, and typed responses.
4
+ */
5
+ /**
6
+ * Maps error conditions to user-friendly error info
7
+ */
8
+ function categorizeError(error, url) {
9
+ if (error instanceof DOMException && error.name === 'AbortError') {
10
+ return {
11
+ type: 'timeout',
12
+ message: 'Request timed out',
13
+ };
14
+ }
15
+ if (error instanceof TypeError) {
16
+ // Network errors like failed to fetch
17
+ return {
18
+ type: 'network',
19
+ message: 'Cannot reach the server',
20
+ original: error,
21
+ };
22
+ }
23
+ if (error instanceof SyntaxError) {
24
+ return {
25
+ type: 'parse',
26
+ message: 'Invalid response from server',
27
+ original: error,
28
+ };
29
+ }
30
+ if (error instanceof Error) {
31
+ return {
32
+ type: 'unknown',
33
+ message: error.message,
34
+ original: error,
35
+ };
36
+ }
37
+ return {
38
+ type: 'unknown',
39
+ message: String(error),
40
+ };
41
+ }
42
+ /**
43
+ * Safe fetch wrapper that never throws.
44
+ * Always returns a FetchResult with either data or error.
45
+ *
46
+ * @param url - The URL to fetch
47
+ * @param options - Fetch options plus optional timeout (default 10s)
48
+ * @returns Promise<FetchResult<T>> - Always resolves, never rejects
49
+ *
50
+ * @example
51
+ * ```typescript
52
+ * const { data, error } = await safeFetch<RouterData>('/api/router');
53
+ * if (error) {
54
+ * // Handle error - render error state
55
+ * return <ErrorState error={error} onRetry={() => refresh()} />;
56
+ * }
57
+ * // Use data safely
58
+ * ```
59
+ */
60
+ export async function safeFetch(url, options = {}) {
61
+ const { timeout = 10000, ...fetchOptions } = options;
62
+ try {
63
+ // Create abort controller for timeout
64
+ const controller = new AbortController();
65
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
66
+ const response = await fetch(url, {
67
+ ...fetchOptions,
68
+ signal: controller.signal,
69
+ });
70
+ clearTimeout(timeoutId);
71
+ if (!response.ok) {
72
+ return {
73
+ data: null,
74
+ error: {
75
+ type: 'http',
76
+ code: response.status,
77
+ message: `HTTP ${response.status}: ${response.statusText}`,
78
+ },
79
+ status: 'error',
80
+ };
81
+ }
82
+ const data = await response.json();
83
+ return {
84
+ data: data,
85
+ error: null,
86
+ status: 'success',
87
+ };
88
+ }
89
+ catch (err) {
90
+ console.error(`Fetch error for ${url}:`, err);
91
+ return {
92
+ data: null,
93
+ error: categorizeError(err, url),
94
+ status: 'error',
95
+ };
96
+ }
97
+ }
98
+ /**
99
+ * POST helper with JSON body
100
+ */
101
+ export async function safePost(url, body, options = {}) {
102
+ return safeFetch(url, {
103
+ ...options,
104
+ method: 'POST',
105
+ headers: {
106
+ 'Content-Type': 'application/json',
107
+ ...options.headers,
108
+ },
109
+ body: JSON.stringify(body),
110
+ });
111
+ }
112
+ /**
113
+ * PUT helper with JSON body
114
+ */
115
+ export async function safePut(url, body, options = {}) {
116
+ return safeFetch(url, {
117
+ ...options,
118
+ method: 'PUT',
119
+ headers: {
120
+ 'Content-Type': 'application/json',
121
+ ...options.headers,
122
+ },
123
+ body: JSON.stringify(body),
124
+ });
125
+ }
126
+ /**
127
+ * DELETE helper
128
+ */
129
+ export async function safeDelete(url, options = {}) {
130
+ return safeFetch(url, {
131
+ ...options,
132
+ method: 'DELETE',
133
+ });
134
+ }
135
+ export default safeFetch;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,215 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+ import { safeFetch, safePost, safePut, safeDelete } from './safeFetch.js';
3
+ describe('safeFetch', () => {
4
+ beforeEach(() => {
5
+ vi.stubGlobal('fetch', vi.fn());
6
+ });
7
+ afterEach(() => {
8
+ vi.unstubAllGlobals();
9
+ });
10
+ describe('successful requests', () => {
11
+ it('returns data on successful response', async () => {
12
+ const mockData = { message: 'success' };
13
+ vi.mocked(fetch).mockResolvedValue({
14
+ ok: true,
15
+ json: () => Promise.resolve(mockData),
16
+ });
17
+ const result = await safeFetch('/api/test');
18
+ expect(result.status).toBe('success');
19
+ expect(result.data).toEqual(mockData);
20
+ expect(result.error).toBeNull();
21
+ });
22
+ it('passes through fetch options', async () => {
23
+ vi.mocked(fetch).mockResolvedValue({
24
+ ok: true,
25
+ json: () => Promise.resolve({}),
26
+ });
27
+ await safeFetch('/api/test', {
28
+ method: 'POST',
29
+ headers: { 'X-Custom': 'value' },
30
+ });
31
+ expect(fetch).toHaveBeenCalledWith('/api/test', expect.objectContaining({
32
+ method: 'POST',
33
+ headers: { 'X-Custom': 'value' },
34
+ }));
35
+ });
36
+ });
37
+ describe('HTTP errors', () => {
38
+ it('handles 404 errors', async () => {
39
+ vi.mocked(fetch).mockResolvedValue({
40
+ ok: false,
41
+ status: 404,
42
+ statusText: 'Not Found',
43
+ });
44
+ const result = await safeFetch('/api/missing');
45
+ expect(result.status).toBe('error');
46
+ expect(result.data).toBeNull();
47
+ expect(result.error).toEqual({
48
+ type: 'http',
49
+ code: 404,
50
+ message: 'HTTP 404: Not Found',
51
+ });
52
+ });
53
+ it('handles 500 errors', async () => {
54
+ vi.mocked(fetch).mockResolvedValue({
55
+ ok: false,
56
+ status: 500,
57
+ statusText: 'Internal Server Error',
58
+ });
59
+ const result = await safeFetch('/api/broken');
60
+ expect(result.status).toBe('error');
61
+ expect(result.error?.type).toBe('http');
62
+ expect(result.error?.code).toBe(500);
63
+ });
64
+ it('handles 401 unauthorized', async () => {
65
+ vi.mocked(fetch).mockResolvedValue({
66
+ ok: false,
67
+ status: 401,
68
+ statusText: 'Unauthorized',
69
+ });
70
+ const result = await safeFetch('/api/protected');
71
+ expect(result.error?.type).toBe('http');
72
+ expect(result.error?.code).toBe(401);
73
+ });
74
+ });
75
+ describe('network errors', () => {
76
+ it('handles network failures', async () => {
77
+ vi.mocked(fetch).mockRejectedValue(new TypeError('Failed to fetch'));
78
+ const result = await safeFetch('/api/unreachable');
79
+ expect(result.status).toBe('error');
80
+ expect(result.error?.type).toBe('network');
81
+ expect(result.error?.message).toBe('Cannot reach the server');
82
+ });
83
+ });
84
+ describe('timeout handling', () => {
85
+ it('times out after specified duration', async () => {
86
+ // Use AbortError to simulate timeout
87
+ vi.mocked(fetch).mockImplementation(() => {
88
+ const error = new DOMException('Aborted', 'AbortError');
89
+ return Promise.reject(error);
90
+ });
91
+ const result = await safeFetch('/api/slow', { timeout: 100 });
92
+ expect(result.status).toBe('error');
93
+ expect(result.error?.type).toBe('timeout');
94
+ expect(result.error?.message).toBe('Request timed out');
95
+ });
96
+ it('uses 10s default timeout', async () => {
97
+ vi.mocked(fetch).mockResolvedValue({
98
+ ok: true,
99
+ json: () => Promise.resolve({}),
100
+ });
101
+ await safeFetch('/api/test');
102
+ // Signal should be passed
103
+ expect(fetch).toHaveBeenCalledWith('/api/test', expect.objectContaining({
104
+ signal: expect.any(AbortSignal),
105
+ }));
106
+ });
107
+ });
108
+ describe('parse errors', () => {
109
+ it('handles invalid JSON response', async () => {
110
+ vi.mocked(fetch).mockResolvedValue({
111
+ ok: true,
112
+ json: () => Promise.reject(new SyntaxError('Unexpected token')),
113
+ });
114
+ const result = await safeFetch('/api/bad-json');
115
+ expect(result.status).toBe('error');
116
+ expect(result.error?.type).toBe('parse');
117
+ expect(result.error?.message).toBe('Invalid response from server');
118
+ });
119
+ });
120
+ describe('never throws', () => {
121
+ it('always returns FetchResult, never throws', async () => {
122
+ // Various error types
123
+ const errorCases = [
124
+ new Error('Generic error'),
125
+ new TypeError('Type error'),
126
+ 'String error',
127
+ undefined,
128
+ null,
129
+ ];
130
+ for (const error of errorCases) {
131
+ vi.mocked(fetch).mockRejectedValue(error);
132
+ // Should not throw
133
+ const result = await safeFetch('/api/test');
134
+ expect(result).toBeDefined();
135
+ expect(result.status).toBe('error');
136
+ expect(result.data).toBeNull();
137
+ expect(result.error).not.toBeNull();
138
+ }
139
+ });
140
+ });
141
+ });
142
+ describe('safePost', () => {
143
+ beforeEach(() => {
144
+ vi.stubGlobal('fetch', vi.fn());
145
+ });
146
+ afterEach(() => {
147
+ vi.unstubAllGlobals();
148
+ });
149
+ it('sends POST request with JSON body', async () => {
150
+ vi.mocked(fetch).mockResolvedValue({
151
+ ok: true,
152
+ json: () => Promise.resolve({ id: 1 }),
153
+ });
154
+ const body = { name: 'test' };
155
+ await safePost('/api/items', body);
156
+ expect(fetch).toHaveBeenCalledWith('/api/items', expect.objectContaining({
157
+ method: 'POST',
158
+ headers: expect.objectContaining({
159
+ 'Content-Type': 'application/json',
160
+ }),
161
+ body: JSON.stringify(body),
162
+ }));
163
+ });
164
+ it('merges custom headers', async () => {
165
+ vi.mocked(fetch).mockResolvedValue({
166
+ ok: true,
167
+ json: () => Promise.resolve({}),
168
+ });
169
+ await safePost('/api/items', {}, { headers: { Authorization: 'Bearer token' } });
170
+ expect(fetch).toHaveBeenCalledWith('/api/items', expect.objectContaining({
171
+ headers: expect.objectContaining({
172
+ 'Content-Type': 'application/json',
173
+ Authorization: 'Bearer token',
174
+ }),
175
+ }));
176
+ });
177
+ });
178
+ describe('safePut', () => {
179
+ beforeEach(() => {
180
+ vi.stubGlobal('fetch', vi.fn());
181
+ });
182
+ afterEach(() => {
183
+ vi.unstubAllGlobals();
184
+ });
185
+ it('sends PUT request with JSON body', async () => {
186
+ vi.mocked(fetch).mockResolvedValue({
187
+ ok: true,
188
+ json: () => Promise.resolve({ updated: true }),
189
+ });
190
+ const body = { name: 'updated' };
191
+ await safePut('/api/items/1', body);
192
+ expect(fetch).toHaveBeenCalledWith('/api/items/1', expect.objectContaining({
193
+ method: 'PUT',
194
+ body: JSON.stringify(body),
195
+ }));
196
+ });
197
+ });
198
+ describe('safeDelete', () => {
199
+ beforeEach(() => {
200
+ vi.stubGlobal('fetch', vi.fn());
201
+ });
202
+ afterEach(() => {
203
+ vi.unstubAllGlobals();
204
+ });
205
+ it('sends DELETE request', async () => {
206
+ vi.mocked(fetch).mockResolvedValue({
207
+ ok: true,
208
+ json: () => Promise.resolve({ deleted: true }),
209
+ });
210
+ await safeDelete('/api/items/1');
211
+ expect(fetch).toHaveBeenCalledWith('/api/items/1', expect.objectContaining({
212
+ method: 'DELETE',
213
+ }));
214
+ });
215
+ });
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Tasks API - Returns tasks from PLAN.md files in flat array format
3
+ */
4
+ import type { Dirent } from 'fs';
5
+ export interface Task {
6
+ id: string;
7
+ title: string;
8
+ status: 'pending' | 'in_progress' | 'completed';
9
+ owner: string | null;
10
+ phase: number;
11
+ }
12
+ export interface FileSystem {
13
+ existsSync: (path: string) => boolean;
14
+ readdir: (path: string, options: {
15
+ withFileTypes: true;
16
+ }) => Promise<Dirent[]>;
17
+ readFile: (path: string, encoding: BufferEncoding) => Promise<string>;
18
+ }
19
+ /**
20
+ * Get all tasks from all PLAN.md files in the planning directory
21
+ * @param projectPath - Root path of the project
22
+ * @param fs - File system implementation (for testing)
23
+ * @returns Array of tasks in flat format, sorted by phase then task number
24
+ */
25
+ export declare function getTasks(projectPath: string, fs?: FileSystem): Promise<Task[]>;
26
+ /**
27
+ * Parse tasks from PLAN.md content
28
+ * @param content - Content of the PLAN.md file
29
+ * @param phaseNum - Phase number for task ID generation
30
+ * @returns Array of tasks parsed from the content
31
+ */
32
+ export declare function parseTasksFromPlan(content: string, phaseNum: number): Task[];
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Tasks API - Returns tasks from PLAN.md files in flat array format
3
+ */
4
+ import { readdir, readFile } from 'fs/promises';
5
+ import { join } from 'path';
6
+ import { existsSync } from 'fs';
7
+ // Default file system implementation
8
+ const defaultFs = {
9
+ existsSync,
10
+ readdir: (path, options) => readdir(path, options),
11
+ readFile: (path, encoding) => readFile(path, encoding),
12
+ };
13
+ /**
14
+ * Get all tasks from all PLAN.md files in the planning directory
15
+ * @param projectPath - Root path of the project
16
+ * @param fs - File system implementation (for testing)
17
+ * @returns Array of tasks in flat format, sorted by phase then task number
18
+ */
19
+ export async function getTasks(projectPath, fs = defaultFs) {
20
+ const phasesDir = join(projectPath, '.planning', 'phases');
21
+ if (!fs.existsSync(phasesDir)) {
22
+ return [];
23
+ }
24
+ const tasks = [];
25
+ try {
26
+ const entries = await fs.readdir(phasesDir, { withFileTypes: true });
27
+ for (const entry of entries) {
28
+ if (entry.isDirectory()) {
29
+ // Extract phase number from directory name (e.g., "39-fix-api" -> 39)
30
+ const dirMatch = entry.name.match(/^(\d+)/);
31
+ if (dirMatch) {
32
+ const phaseNum = parseInt(dirMatch[1], 10);
33
+ const planPath = join(phasesDir, entry.name, `${dirMatch[1]}-PLAN.md`);
34
+ if (fs.existsSync(planPath)) {
35
+ try {
36
+ const content = await fs.readFile(planPath, 'utf-8');
37
+ const phaseTasks = parseTasksFromPlan(content, phaseNum);
38
+ tasks.push(...phaseTasks);
39
+ }
40
+ catch {
41
+ // Skip unreadable files
42
+ }
43
+ }
44
+ }
45
+ }
46
+ }
47
+ }
48
+ catch {
49
+ // Return empty if can't read directory
50
+ return [];
51
+ }
52
+ // Sort by phase number, then by task number
53
+ tasks.sort((a, b) => {
54
+ if (a.phase !== b.phase) {
55
+ return a.phase - b.phase;
56
+ }
57
+ // Extract task number from id (e.g., "39-1" -> 1)
58
+ const aTaskNum = parseInt(a.id.split('-')[1], 10);
59
+ const bTaskNum = parseInt(b.id.split('-')[1], 10);
60
+ return aTaskNum - bTaskNum;
61
+ });
62
+ return tasks;
63
+ }
64
+ /**
65
+ * Parse tasks from PLAN.md content
66
+ * @param content - Content of the PLAN.md file
67
+ * @param phaseNum - Phase number for task ID generation
68
+ * @returns Array of tasks parsed from the content
69
+ */
70
+ export function parseTasksFromPlan(content, phaseNum) {
71
+ const tasks = [];
72
+ const lines = content.split('\n');
73
+ for (const line of lines) {
74
+ // Match task headers like:
75
+ // "### Task 1: Setup Project [x@alice]"
76
+ // "### Task 1: Setup Project [>]"
77
+ // "### Task 1: Setup Project [ ]"
78
+ // "### Task 1: Name [x]"
79
+ // Requires colon after task number
80
+ const taskMatch = line.match(/^###\s*Task\s+(\d+):\s*(.+?)\s*\[([x> ]?)(?:@(\w+))?\]\s*$/i);
81
+ if (taskMatch) {
82
+ const taskNum = parseInt(taskMatch[1], 10);
83
+ const title = taskMatch[2].trim();
84
+ const statusChar = taskMatch[3].trim();
85
+ const owner = taskMatch[4] || null;
86
+ const status = statusChar === 'x' ? 'completed' :
87
+ statusChar === '>' ? 'in_progress' : 'pending';
88
+ tasks.push({
89
+ id: `${phaseNum}-${taskNum}`,
90
+ title,
91
+ status,
92
+ owner,
93
+ phase: phaseNum,
94
+ });
95
+ }
96
+ }
97
+ return tasks;
98
+ }
@@ -0,0 +1 @@
1
+ export {};