cache-overflow-mcp 0.3.4 → 0.3.5
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/.env.example +3 -3
- package/AGENTS.md +24 -3
- package/README.md +59 -0
- package/TROUBLESHOOTING.md +219 -0
- package/dist/cli.js +13 -1
- package/dist/cli.js.map +1 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +53 -9
- package/dist/client.js.map +1 -1
- package/dist/config.d.ts +3 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +5 -0
- package/dist/config.js.map +1 -1
- package/dist/logger.d.ts +16 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +127 -0
- package/dist/logger.js.map +1 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +50 -7
- package/dist/server.js.map +1 -1
- package/dist/testing/mock-data.js +40 -40
- package/dist/ui/verification-dialog.d.ts.map +1 -1
- package/dist/ui/verification-dialog.js +307 -268
- package/dist/ui/verification-dialog.js.map +1 -1
- package/package.json +3 -2
- package/scripts/view-logs.js +125 -0
- package/src/cli.ts +23 -10
- package/src/client.test.ts +116 -116
- package/src/client.ts +122 -76
- package/src/config.ts +14 -9
- package/src/index.ts +3 -3
- package/src/logger.ts +150 -0
- package/src/server.ts +49 -7
- package/src/testing/mock-data.ts +142 -142
- package/src/testing/mock-server.ts +176 -176
- package/src/tools/index.ts +23 -23
- package/src/types.ts +39 -39
- package/src/ui/verification-dialog.ts +382 -342
- package/tsconfig.json +20 -20
- package/dist/tools/get-balance.d.ts +0 -3
- package/dist/tools/get-balance.d.ts.map +0 -1
- package/dist/tools/get-balance.js +0 -34
- package/dist/tools/get-balance.js.map +0 -1
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Simple script to help users locate and view their cache.overflow MCP logs
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import fs from 'fs';
|
|
8
|
+
import path from 'path';
|
|
9
|
+
import os from 'os';
|
|
10
|
+
|
|
11
|
+
const homeLogPath = path.join(os.homedir(), '.cache-overflow', 'cache-overflow-mcp.log');
|
|
12
|
+
const tempLogPath = path.join(os.tmpdir(), 'cache-overflow', 'cache-overflow-mcp.log');
|
|
13
|
+
const customLogPath = process.env.CACHE_OVERFLOW_LOG_DIR
|
|
14
|
+
? path.join(process.env.CACHE_OVERFLOW_LOG_DIR, 'cache-overflow-mcp.log')
|
|
15
|
+
: null;
|
|
16
|
+
|
|
17
|
+
console.log('🔍 Looking for cache.overflow MCP log file...\n');
|
|
18
|
+
|
|
19
|
+
// Check custom location first
|
|
20
|
+
if (customLogPath && fs.existsSync(customLogPath)) {
|
|
21
|
+
console.log('✅ Found log file at custom location:');
|
|
22
|
+
console.log(` ${customLogPath}\n`);
|
|
23
|
+
displayLog(customLogPath);
|
|
24
|
+
}
|
|
25
|
+
// Check home directory
|
|
26
|
+
else if (fs.existsSync(homeLogPath)) {
|
|
27
|
+
console.log('✅ Found log file at default location:');
|
|
28
|
+
console.log(` ${homeLogPath}\n`);
|
|
29
|
+
displayLog(homeLogPath);
|
|
30
|
+
}
|
|
31
|
+
// Check temp directory
|
|
32
|
+
else if (fs.existsSync(tempLogPath)) {
|
|
33
|
+
console.log('✅ Found log file at fallback location:');
|
|
34
|
+
console.log(` ${tempLogPath}\n`);
|
|
35
|
+
displayLog(tempLogPath);
|
|
36
|
+
}
|
|
37
|
+
// No log file found
|
|
38
|
+
else {
|
|
39
|
+
console.log('❌ No log file found. Checked locations:');
|
|
40
|
+
console.log(` ${homeLogPath}`);
|
|
41
|
+
console.log(` ${tempLogPath}`);
|
|
42
|
+
if (customLogPath) {
|
|
43
|
+
console.log(` ${customLogPath}`);
|
|
44
|
+
}
|
|
45
|
+
console.log('\nThe log file is created when the MCP server starts.');
|
|
46
|
+
console.log('Make sure cache.overflow-mcp has been run at least once.\n');
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function displayLog(logPath) {
|
|
51
|
+
const stats = fs.statSync(logPath);
|
|
52
|
+
const sizeKB = (stats.size / 1024).toFixed(2);
|
|
53
|
+
|
|
54
|
+
console.log(`📊 File size: ${sizeKB} KB`);
|
|
55
|
+
console.log(`🕐 Last modified: ${stats.mtime.toLocaleString()}\n`);
|
|
56
|
+
|
|
57
|
+
// Read the file
|
|
58
|
+
const content = fs.readFileSync(logPath, 'utf-8');
|
|
59
|
+
const lines = content.trim().split('\n').filter(line => line.trim());
|
|
60
|
+
|
|
61
|
+
console.log(`📝 Total log entries: ${lines.length}\n`);
|
|
62
|
+
|
|
63
|
+
// Count by level
|
|
64
|
+
const levels = { ERROR: 0, WARN: 0, INFO: 0 };
|
|
65
|
+
lines.forEach(line => {
|
|
66
|
+
try {
|
|
67
|
+
const entry = JSON.parse(line);
|
|
68
|
+
if (entry.level in levels) {
|
|
69
|
+
levels[entry.level]++;
|
|
70
|
+
}
|
|
71
|
+
} catch (e) {
|
|
72
|
+
// Skip invalid JSON lines
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
console.log('📈 Log level breakdown:');
|
|
77
|
+
console.log(` ERROR: ${levels.ERROR}`);
|
|
78
|
+
console.log(` WARN: ${levels.WARN}`);
|
|
79
|
+
console.log(` INFO: ${levels.INFO}\n`);
|
|
80
|
+
|
|
81
|
+
// Show last 10 entries
|
|
82
|
+
const recentLines = lines.slice(-10);
|
|
83
|
+
console.log('📋 Last 10 log entries:\n');
|
|
84
|
+
console.log('─'.repeat(80));
|
|
85
|
+
|
|
86
|
+
recentLines.forEach(line => {
|
|
87
|
+
try {
|
|
88
|
+
const entry = JSON.parse(line);
|
|
89
|
+
const timestamp = new Date(entry.timestamp).toLocaleString();
|
|
90
|
+
const level = entry.level.padEnd(5);
|
|
91
|
+
const icon = entry.level === 'ERROR' ? '❌' : entry.level === 'WARN' ? '⚠️' : 'ℹ️';
|
|
92
|
+
|
|
93
|
+
console.log(`${icon} [${timestamp}] ${level} ${entry.message}`);
|
|
94
|
+
|
|
95
|
+
if (entry.error) {
|
|
96
|
+
console.log(` Error: ${entry.error.name}: ${entry.error.message}`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (entry.context && Object.keys(entry.context).length > 0) {
|
|
100
|
+
console.log(` Context: ${JSON.stringify(entry.context)}`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
console.log('─'.repeat(80));
|
|
104
|
+
} catch (e) {
|
|
105
|
+
console.log(line);
|
|
106
|
+
console.log('─'.repeat(80));
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
console.log(`\n💡 To view the full log file, run:`);
|
|
111
|
+
if (process.platform === 'win32') {
|
|
112
|
+
console.log(` type "${logPath}"`);
|
|
113
|
+
} else {
|
|
114
|
+
console.log(` cat "${logPath}"`);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
console.log(`\n💡 To follow logs in real-time, run:`);
|
|
118
|
+
if (process.platform === 'win32') {
|
|
119
|
+
console.log(` Get-Content "${logPath}" -Wait`);
|
|
120
|
+
} else {
|
|
121
|
+
console.log(` tail -f "${logPath}"`);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
console.log('');
|
|
125
|
+
}
|
package/src/cli.ts
CHANGED
|
@@ -1,10 +1,23 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import { CacheOverflowServer } from './server.js';
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { CacheOverflowServer } from './server.js';
|
|
4
|
+
import { logger } from './logger.js';
|
|
5
|
+
|
|
6
|
+
async function main() {
|
|
7
|
+
// Log startup information
|
|
8
|
+
logger.logStartup();
|
|
9
|
+
logger.info(`Log file location: ${logger.getLogFilePath()}`);
|
|
10
|
+
|
|
11
|
+
const server = new CacheOverflowServer();
|
|
12
|
+
await server.start();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
main().catch((error) => {
|
|
16
|
+
logger.error('Fatal error during server startup', error, {
|
|
17
|
+
errorType: 'STARTUP_FAILURE',
|
|
18
|
+
});
|
|
19
|
+
console.error('Fatal error:', error);
|
|
20
|
+
console.error(`\nError details have been logged to: ${logger.getLogFilePath()}`);
|
|
21
|
+
console.error('Please send this log file when reporting issues.');
|
|
22
|
+
process.exit(1);
|
|
23
|
+
});
|
package/src/client.test.ts
CHANGED
|
@@ -1,116 +1,116 @@
|
|
|
1
|
-
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
|
2
|
-
import { CacheOverflowClient } from './client.js';
|
|
3
|
-
import { MockServer } from './testing/mock-server.js';
|
|
4
|
-
import { mockSolutions, mockFindResults } from './testing/mock-data.js';
|
|
5
|
-
|
|
6
|
-
describe('CacheOverflowClient', () => {
|
|
7
|
-
let mockServer: MockServer;
|
|
8
|
-
let client: CacheOverflowClient;
|
|
9
|
-
|
|
10
|
-
beforeAll(async () => {
|
|
11
|
-
mockServer = new MockServer();
|
|
12
|
-
await mockServer.start();
|
|
13
|
-
client = new CacheOverflowClient(mockServer.url);
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
afterAll(async () => {
|
|
17
|
-
await mockServer.stop();
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
describe('findSolution', () => {
|
|
21
|
-
it('should return search results', async () => {
|
|
22
|
-
const result = await client.findSolution('binary search');
|
|
23
|
-
|
|
24
|
-
expect(result.success).toBe(true);
|
|
25
|
-
if (result.success) {
|
|
26
|
-
expect(Array.isArray(result.data)).toBe(true);
|
|
27
|
-
expect(result.data.length).toBeGreaterThan(0);
|
|
28
|
-
expect(result.data[0]).toHaveProperty('solution_id');
|
|
29
|
-
expect(result.data[0]).toHaveProperty('query_title');
|
|
30
|
-
expect(result.data[0]).toHaveProperty('human_verification_required');
|
|
31
|
-
}
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
it('should return results matching the query', async () => {
|
|
35
|
-
const result = await client.findSolution('TypeScript');
|
|
36
|
-
|
|
37
|
-
expect(result.success).toBe(true);
|
|
38
|
-
if (result.success) {
|
|
39
|
-
const hasTypeScript = result.data.some((r) =>
|
|
40
|
-
r.query_title.toLowerCase().includes('typescript')
|
|
41
|
-
);
|
|
42
|
-
expect(hasTypeScript).toBe(true);
|
|
43
|
-
}
|
|
44
|
-
});
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
describe('unlockSolution', () => {
|
|
48
|
-
it('should return the unlocked solution', async () => {
|
|
49
|
-
const result = await client.unlockSolution(mockSolutions[0].id);
|
|
50
|
-
|
|
51
|
-
expect(result.success).toBe(true);
|
|
52
|
-
if (result.success) {
|
|
53
|
-
expect(result.data).toHaveProperty('id');
|
|
54
|
-
expect(result.data).toHaveProperty('solution_body');
|
|
55
|
-
expect(result.data).toHaveProperty('price_current');
|
|
56
|
-
expect(result.data).toHaveProperty('verification_state');
|
|
57
|
-
}
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
it('should return a solution even for unknown IDs', async () => {
|
|
61
|
-
const result = await client.unlockSolution('unknown_id');
|
|
62
|
-
|
|
63
|
-
expect(result.success).toBe(true);
|
|
64
|
-
if (result.success) {
|
|
65
|
-
expect(result.data).toHaveProperty('id');
|
|
66
|
-
}
|
|
67
|
-
});
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
describe('publishSolution', () => {
|
|
71
|
-
it('should create a new solution', async () => {
|
|
72
|
-
const result = await client.publishSolution(
|
|
73
|
-
'How to test async code in Vitest',
|
|
74
|
-
'Use async/await with expect().resolves or expect().rejects'
|
|
75
|
-
);
|
|
76
|
-
|
|
77
|
-
expect(result.success).toBe(true);
|
|
78
|
-
if (result.success) {
|
|
79
|
-
expect(result.data).toHaveProperty('id');
|
|
80
|
-
expect(result.data.query_title).toBe('How to test async code in Vitest');
|
|
81
|
-
expect(result.data.solution_body).toBe(
|
|
82
|
-
'Use async/await with expect().resolves or expect().rejects'
|
|
83
|
-
);
|
|
84
|
-
expect(result.data.verification_state).toBe('PENDING');
|
|
85
|
-
}
|
|
86
|
-
});
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
describe('submitVerification', () => {
|
|
90
|
-
it('should submit verification successfully', async () => {
|
|
91
|
-
const result = await client.submitVerification(mockSolutions[0].id, true);
|
|
92
|
-
|
|
93
|
-
expect(result.success).toBe(true);
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
it('should allow marking as unsafe', async () => {
|
|
97
|
-
const result = await client.submitVerification(mockSolutions[0].id, false);
|
|
98
|
-
|
|
99
|
-
expect(result.success).toBe(true);
|
|
100
|
-
});
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
describe('submitFeedback', () => {
|
|
104
|
-
it('should submit positive feedback', async () => {
|
|
105
|
-
const result = await client.submitFeedback(mockSolutions[0].id, true);
|
|
106
|
-
|
|
107
|
-
expect(result.success).toBe(true);
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
it('should submit negative feedback', async () => {
|
|
111
|
-
const result = await client.submitFeedback(mockSolutions[0].id, false);
|
|
112
|
-
|
|
113
|
-
expect(result.success).toBe(true);
|
|
114
|
-
});
|
|
115
|
-
});
|
|
116
|
-
});
|
|
1
|
+
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
|
2
|
+
import { CacheOverflowClient } from './client.js';
|
|
3
|
+
import { MockServer } from './testing/mock-server.js';
|
|
4
|
+
import { mockSolutions, mockFindResults } from './testing/mock-data.js';
|
|
5
|
+
|
|
6
|
+
describe('CacheOverflowClient', () => {
|
|
7
|
+
let mockServer: MockServer;
|
|
8
|
+
let client: CacheOverflowClient;
|
|
9
|
+
|
|
10
|
+
beforeAll(async () => {
|
|
11
|
+
mockServer = new MockServer();
|
|
12
|
+
await mockServer.start();
|
|
13
|
+
client = new CacheOverflowClient(mockServer.url);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
afterAll(async () => {
|
|
17
|
+
await mockServer.stop();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
describe('findSolution', () => {
|
|
21
|
+
it('should return search results', async () => {
|
|
22
|
+
const result = await client.findSolution('binary search');
|
|
23
|
+
|
|
24
|
+
expect(result.success).toBe(true);
|
|
25
|
+
if (result.success) {
|
|
26
|
+
expect(Array.isArray(result.data)).toBe(true);
|
|
27
|
+
expect(result.data.length).toBeGreaterThan(0);
|
|
28
|
+
expect(result.data[0]).toHaveProperty('solution_id');
|
|
29
|
+
expect(result.data[0]).toHaveProperty('query_title');
|
|
30
|
+
expect(result.data[0]).toHaveProperty('human_verification_required');
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('should return results matching the query', async () => {
|
|
35
|
+
const result = await client.findSolution('TypeScript');
|
|
36
|
+
|
|
37
|
+
expect(result.success).toBe(true);
|
|
38
|
+
if (result.success) {
|
|
39
|
+
const hasTypeScript = result.data.some((r) =>
|
|
40
|
+
r.query_title.toLowerCase().includes('typescript')
|
|
41
|
+
);
|
|
42
|
+
expect(hasTypeScript).toBe(true);
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe('unlockSolution', () => {
|
|
48
|
+
it('should return the unlocked solution', async () => {
|
|
49
|
+
const result = await client.unlockSolution(mockSolutions[0].id);
|
|
50
|
+
|
|
51
|
+
expect(result.success).toBe(true);
|
|
52
|
+
if (result.success) {
|
|
53
|
+
expect(result.data).toHaveProperty('id');
|
|
54
|
+
expect(result.data).toHaveProperty('solution_body');
|
|
55
|
+
expect(result.data).toHaveProperty('price_current');
|
|
56
|
+
expect(result.data).toHaveProperty('verification_state');
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('should return a solution even for unknown IDs', async () => {
|
|
61
|
+
const result = await client.unlockSolution('unknown_id');
|
|
62
|
+
|
|
63
|
+
expect(result.success).toBe(true);
|
|
64
|
+
if (result.success) {
|
|
65
|
+
expect(result.data).toHaveProperty('id');
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
describe('publishSolution', () => {
|
|
71
|
+
it('should create a new solution', async () => {
|
|
72
|
+
const result = await client.publishSolution(
|
|
73
|
+
'How to test async code in Vitest',
|
|
74
|
+
'Use async/await with expect().resolves or expect().rejects'
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
expect(result.success).toBe(true);
|
|
78
|
+
if (result.success) {
|
|
79
|
+
expect(result.data).toHaveProperty('id');
|
|
80
|
+
expect(result.data.query_title).toBe('How to test async code in Vitest');
|
|
81
|
+
expect(result.data.solution_body).toBe(
|
|
82
|
+
'Use async/await with expect().resolves or expect().rejects'
|
|
83
|
+
);
|
|
84
|
+
expect(result.data.verification_state).toBe('PENDING');
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
describe('submitVerification', () => {
|
|
90
|
+
it('should submit verification successfully', async () => {
|
|
91
|
+
const result = await client.submitVerification(mockSolutions[0].id, true);
|
|
92
|
+
|
|
93
|
+
expect(result.success).toBe(true);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('should allow marking as unsafe', async () => {
|
|
97
|
+
const result = await client.submitVerification(mockSolutions[0].id, false);
|
|
98
|
+
|
|
99
|
+
expect(result.success).toBe(true);
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
describe('submitFeedback', () => {
|
|
104
|
+
it('should submit positive feedback', async () => {
|
|
105
|
+
const result = await client.submitFeedback(mockSolutions[0].id, true);
|
|
106
|
+
|
|
107
|
+
expect(result.success).toBe(true);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('should submit negative feedback', async () => {
|
|
111
|
+
const result = await client.submitFeedback(mockSolutions[0].id, false);
|
|
112
|
+
|
|
113
|
+
expect(result.success).toBe(true);
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
});
|
package/src/client.ts
CHANGED
|
@@ -1,76 +1,122 @@
|
|
|
1
|
-
import { ApiResponse, Solution, FindSolutionResult } from './types.js';
|
|
2
|
-
import { config } from './config.js';
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
private
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
this.
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
})
|
|
75
|
-
|
|
76
|
-
|
|
1
|
+
import { ApiResponse, Solution, FindSolutionResult } from './types.js';
|
|
2
|
+
import { config } from './config.js';
|
|
3
|
+
import { logger } from './logger.js';
|
|
4
|
+
|
|
5
|
+
export class CacheOverflowClient {
|
|
6
|
+
private apiUrl: string;
|
|
7
|
+
private authToken: string | undefined;
|
|
8
|
+
|
|
9
|
+
constructor(apiUrl?: string) {
|
|
10
|
+
this.apiUrl = apiUrl ?? config.api.url;
|
|
11
|
+
this.authToken = config.auth.token;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
private async request<T>(
|
|
15
|
+
method: string,
|
|
16
|
+
path: string,
|
|
17
|
+
body?: unknown
|
|
18
|
+
): Promise<ApiResponse<T>> {
|
|
19
|
+
const headers: Record<string, string> = {
|
|
20
|
+
'Content-Type': 'application/json',
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
if (this.authToken) {
|
|
24
|
+
headers['Authorization'] = `Bearer ${this.authToken}`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const url = `${this.apiUrl}${path}`;
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
const response = await fetch(url, {
|
|
31
|
+
method,
|
|
32
|
+
headers,
|
|
33
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// Read response as text first, then try to parse as JSON
|
|
37
|
+
const textResponse = await response.text();
|
|
38
|
+
const contentType = response.headers.get('content-type');
|
|
39
|
+
let data: Record<string, unknown>;
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
data = JSON.parse(textResponse) as Record<string, unknown>;
|
|
43
|
+
} catch (jsonError) {
|
|
44
|
+
// If JSON parsing fails, log and return the text as error
|
|
45
|
+
logger.error('API returned non-JSON response', jsonError as Error, {
|
|
46
|
+
method,
|
|
47
|
+
path,
|
|
48
|
+
statusCode: response.status,
|
|
49
|
+
contentType,
|
|
50
|
+
responseText: textResponse.substring(0, 200), // Log first 200 chars
|
|
51
|
+
errorType: 'INVALID_JSON_RESPONSE',
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// Return the text as an error message
|
|
55
|
+
return {
|
|
56
|
+
success: false,
|
|
57
|
+
error: textResponse || 'Invalid response from server'
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (!response.ok) {
|
|
62
|
+
const errorMessage = (data.error as string) ?? 'Unknown error';
|
|
63
|
+
logger.error('API request failed', undefined, {
|
|
64
|
+
method,
|
|
65
|
+
path,
|
|
66
|
+
statusCode: response.status,
|
|
67
|
+
errorMessage,
|
|
68
|
+
errorType: 'API_ERROR',
|
|
69
|
+
});
|
|
70
|
+
return { success: false, error: errorMessage };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return { success: true, data: data as T };
|
|
74
|
+
} catch (error) {
|
|
75
|
+
logger.error('Network or fetch error during API request', error as Error, {
|
|
76
|
+
method,
|
|
77
|
+
path,
|
|
78
|
+
url,
|
|
79
|
+
errorType: 'NETWORK_ERROR',
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// Re-throw network errors so they can be handled by the caller
|
|
83
|
+
throw error;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async findSolution(query: string): Promise<ApiResponse<FindSolutionResult[]>> {
|
|
88
|
+
return this.request('GET', `/solutions/search?query=${encodeURIComponent(query)}`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async unlockSolution(solutionId: string): Promise<ApiResponse<Solution>> {
|
|
92
|
+
return this.request('POST', `/solutions/${solutionId}/unlock`);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async publishSolution(
|
|
96
|
+
queryTitle: string,
|
|
97
|
+
solutionBody: string
|
|
98
|
+
): Promise<ApiResponse<Solution>> {
|
|
99
|
+
return this.request('POST', '/solutions', {
|
|
100
|
+
query_title: queryTitle,
|
|
101
|
+
solution_body: solutionBody,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async submitVerification(
|
|
106
|
+
solutionId: string,
|
|
107
|
+
isSafe: boolean
|
|
108
|
+
): Promise<ApiResponse<void>> {
|
|
109
|
+
return this.request('POST', `/solutions/${solutionId}/verify`, {
|
|
110
|
+
is_safe: isSafe,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async submitFeedback(
|
|
115
|
+
solutionId: string,
|
|
116
|
+
isUseful: boolean
|
|
117
|
+
): Promise<ApiResponse<void>> {
|
|
118
|
+
return this.request('POST', `/solutions/${solutionId}/feedback`, {
|
|
119
|
+
is_useful: isUseful,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
}
|
package/src/config.ts
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
|
-
export const config = {
|
|
2
|
-
api: {
|
|
3
|
-
url: process.env.CACHE_OVERFLOW_URL ?? 'https://cache-overflow.onrender.com/api',
|
|
4
|
-
timeout: parseInt(process.env.CACHE_OVERFLOW_TIMEOUT ?? '30000'),
|
|
5
|
-
},
|
|
6
|
-
auth: {
|
|
7
|
-
token: process.env.CACHE_OVERFLOW_TOKEN,
|
|
8
|
-
},
|
|
9
|
-
|
|
1
|
+
export const config = {
|
|
2
|
+
api: {
|
|
3
|
+
url: process.env.CACHE_OVERFLOW_URL ?? 'https://cache-overflow.onrender.com/api',
|
|
4
|
+
timeout: parseInt(process.env.CACHE_OVERFLOW_TIMEOUT ?? '30000'),
|
|
5
|
+
},
|
|
6
|
+
auth: {
|
|
7
|
+
token: process.env.CACHE_OVERFLOW_TOKEN,
|
|
8
|
+
},
|
|
9
|
+
logging: {
|
|
10
|
+
// Directory where log files will be written
|
|
11
|
+
// Default: ~/.cache-overflow/ (or temp directory if home is not writable)
|
|
12
|
+
logDir: process.env.CACHE_OVERFLOW_LOG_DIR,
|
|
13
|
+
},
|
|
14
|
+
};
|
package/src/index.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export { CacheOverflowServer } from './server.js';
|
|
2
|
-
export { CacheOverflowClient } from './client.js';
|
|
3
|
-
export * from './types.js';
|
|
1
|
+
export { CacheOverflowServer } from './server.js';
|
|
2
|
+
export { CacheOverflowClient } from './client.js';
|
|
3
|
+
export * from './types.js';
|