@alex900530/claude-persistent-memory 1.0.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/lib/utils.js ADDED
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Minimal utility functions for the persistent memory system.
3
+ * Extracted from .claude/scripts/lib/utils.js - only memory-relevant utilities.
4
+ */
5
+
6
+ const fs = require('fs');
7
+ const path = require('path');
8
+
9
+ /**
10
+ * Ensure a directory exists (create recursively if not).
11
+ * @param {string} dirPath - Directory path to ensure
12
+ * @returns {string} The directory path
13
+ */
14
+ function ensureDir(dirPath) {
15
+ if (!fs.existsSync(dirPath)) {
16
+ fs.mkdirSync(dirPath, { recursive: true });
17
+ }
18
+ return dirPath;
19
+ }
20
+
21
+ /**
22
+ * Calculate cosine similarity between two vectors.
23
+ * @param {number[]} vec1 - First vector
24
+ * @param {number[]} vec2 - Second vector
25
+ * @returns {number} Similarity score between 0 and 1
26
+ */
27
+ function cosineSimilarity(vec1, vec2) {
28
+ if (!vec1 || !vec2 || vec1.length !== vec2.length) return 0;
29
+
30
+ let dotProduct = 0;
31
+ let norm1 = 0;
32
+ let norm2 = 0;
33
+
34
+ for (let i = 0; i < vec1.length; i++) {
35
+ dotProduct += vec1[i] * vec2[i];
36
+ norm1 += vec1[i] * vec1[i];
37
+ norm2 += vec2[i] * vec2[i];
38
+ }
39
+
40
+ const magnitude = Math.sqrt(norm1) * Math.sqrt(norm2);
41
+ return magnitude === 0 ? 0 : dotProduct / magnitude;
42
+ }
43
+
44
+ /**
45
+ * Log to stderr (visible to user in Claude Code, does not pollute stdout).
46
+ * @param {string} message - Message to log
47
+ */
48
+ function log(message) {
49
+ console.error(message);
50
+ }
51
+
52
+ /**
53
+ * Read a text file safely. Returns null if file does not exist or read fails.
54
+ * @param {string} filePath - Absolute path to the file
55
+ * @returns {string|null} File contents or null
56
+ */
57
+ function readFile(filePath) {
58
+ try {
59
+ return fs.readFileSync(filePath, 'utf8');
60
+ } catch {
61
+ return null;
62
+ }
63
+ }
64
+
65
+ /**
66
+ * Write a text file. Parent directories are created automatically.
67
+ * @param {string} filePath - Absolute path to the file
68
+ * @param {string} content - Content to write
69
+ */
70
+ function writeFile(filePath, content) {
71
+ ensureDir(path.dirname(filePath));
72
+ fs.writeFileSync(filePath, content, 'utf8');
73
+ }
74
+
75
+ /**
76
+ * Append to a text file. Parent directories are created automatically.
77
+ * @param {string} filePath - Absolute path to the file
78
+ * @param {string} content - Content to append
79
+ */
80
+ function appendFile(filePath, content) {
81
+ ensureDir(path.dirname(filePath));
82
+ fs.appendFileSync(filePath, content, 'utf8');
83
+ }
84
+
85
+ module.exports = {
86
+ ensureDir,
87
+ cosineSimilarity,
88
+ log,
89
+ readFile,
90
+ writeFile,
91
+ appendFile,
92
+ };
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "@alex900530/claude-persistent-memory",
3
+ "version": "1.0.0",
4
+ "description": "Persistent memory system for Claude Code — hybrid BM25 + vector search, LLM-driven structuring, automatic clustering",
5
+ "main": "lib/memory-db.js",
6
+ "bin": {
7
+ "claude-persistent-memory": "bin/setup.js"
8
+ },
9
+ "scripts": {
10
+ "postinstall": "node bin/setup.js --postinstall",
11
+ "setup": "node bin/setup.js",
12
+ "uninstall": "node bin/uninstall.js",
13
+ "embedding-server": "node services/embedding-server.js",
14
+ "llm-server": "node services/llm-server.js",
15
+ "mcp-server": "node services/memory-mcp-server.js",
16
+ "rebuild-vectors": "node tools/rebuild-vectors.js"
17
+ },
18
+ "files": [
19
+ "bin/",
20
+ "hooks/",
21
+ "lib/",
22
+ "services/",
23
+ "tools/",
24
+ "config.js",
25
+ "config.default.js"
26
+ ],
27
+ "publishConfig": {
28
+ "registry": "https://registry.npmjs.org"
29
+ },
30
+ "engines": {
31
+ "node": ">=18"
32
+ },
33
+ "dependencies": {
34
+ "@huggingface/transformers": "^3.8.1",
35
+ "@modelcontextprotocol/sdk": "^1.26.0",
36
+ "better-sqlite3": "^12.6.2",
37
+ "sqlite-vec": "^0.1.7-alpha.2",
38
+ "zod": "^3.24.0"
39
+ },
40
+ "optionalDependencies": {
41
+ "nodejieba": "^2.6.0"
42
+ },
43
+ "license": "MIT"
44
+ }
@@ -0,0 +1,108 @@
1
+ #!/usr/bin/env node
2
+ const net = require('net');
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const config = require('../config');
6
+ const { ensureDir } = require('../lib/utils');
7
+
8
+ const PORT = config.embeddingPort;
9
+ const PID_FILE = path.join(config.pidDir, 'claude-embedding.pid');
10
+
11
+ let memoryDb = null;
12
+ let isReady = false;
13
+ let server = null;
14
+
15
+ const LOG_FILE = path.join(config.logDir, 'embedding-server.log');
16
+ ensureDir(config.logDir);
17
+
18
+ function str(v) { return v == null ? '' : typeof v === 'string' ? v : JSON.stringify(v); }
19
+ function log(msg) { const line = `[${new Date().toISOString()}] ${msg}\n`; try { fs.appendFileSync(LOG_FILE, line); } catch (e) {} }
20
+
21
+ async function initialize() {
22
+ console.error('[EmbeddingServer] Starting...');
23
+ try {
24
+ memoryDb = require('../lib/memory-db');
25
+ if (memoryDb.warmupEmbedding) {
26
+ console.error('[EmbeddingServer] Warming up embedding model...');
27
+ await memoryDb.warmupEmbedding();
28
+ console.error('[EmbeddingServer] Embedding model ready');
29
+ }
30
+ isReady = true;
31
+ } catch (e) {
32
+ console.error('[EmbeddingServer] Failed to initialize:', e.message);
33
+ process.exit(1);
34
+ }
35
+ }
36
+
37
+ async function handleRequest(data) {
38
+ if (!isReady) return { success: false, error: 'Server not ready' };
39
+ try {
40
+ const request = JSON.parse(data);
41
+ const startTime = Date.now();
42
+ switch (request.action) {
43
+ case 'search': {
44
+ log(`[REQ] action=search query=${str(request.query)} limit=${request.limit || 3}`);
45
+ const results = await memoryDb.search(request.query, request.limit || 3, request.options || {});
46
+ const duration = Date.now() - startTime;
47
+ log(`[RES] action=search duration=${duration}ms results=${results.length} matches=${results.map(r => '#' + r.id + '(' + (r.vectorSimilarity != null ? r.vectorSimilarity.toFixed(3) : '?') + ')').join(',')}`);
48
+ return { success: true, results };
49
+ }
50
+ case 'quickSearch': {
51
+ log(`[REQ] action=quickSearch query=${str(request.query)} limit=${request.limit || 3}`);
52
+ const quickResults = memoryDb.quickSearch(request.query, request.limit || 3, request.options || {});
53
+ const duration = Date.now() - startTime;
54
+ log(`[RES] action=quickSearch duration=${duration}ms results=${quickResults.length} matches=${quickResults.map(r => '#' + r.id + '(' + (r.vectorSimilarity != null ? r.vectorSimilarity.toFixed(3) : '?') + ')').join(',')}`);
55
+ return { success: true, results: quickResults };
56
+ }
57
+ case 'ping': return { success: true, message: 'pong', ready: isReady };
58
+ case 'shutdown':
59
+ console.error('[EmbeddingServer] Shutdown requested');
60
+ setTimeout(() => { cleanup(); process.exit(0); }, 100);
61
+ return { success: true, message: 'Shutting down' };
62
+ case 'stats':
63
+ const stats = memoryDb.getStats();
64
+ return { success: true, stats };
65
+ default: return { success: false, error: `Unknown action: ${request.action}` };
66
+ }
67
+ } catch (e) {
68
+ log(`[ERR] error=${e.message}`);
69
+ return { success: false, error: e.message };
70
+ }
71
+ }
72
+
73
+ function cleanup() {
74
+ try { if (fs.existsSync(PID_FILE)) fs.unlinkSync(PID_FILE); if (memoryDb && memoryDb.closeDb) memoryDb.closeDb(); } catch (e) {}
75
+ }
76
+
77
+ async function startServer() {
78
+ await initialize();
79
+ server = net.createServer((socket) => {
80
+ let buffer = '';
81
+ socket.on('data', async (chunk) => {
82
+ buffer += chunk.toString();
83
+ const lines = buffer.split('\n');
84
+ buffer = lines.pop();
85
+ for (const line of lines) {
86
+ if (line.trim()) {
87
+ const response = await handleRequest(line);
88
+ socket.write(JSON.stringify(response) + '\n');
89
+ }
90
+ }
91
+ });
92
+ socket.on('error', () => {});
93
+ });
94
+ server.on('error', (err) => {
95
+ if (err.code === 'EADDRINUSE') { console.error(`[EmbeddingServer] Port ${PORT} already in use`); process.exit(0); }
96
+ console.error('[EmbeddingServer] Server error:', err.message);
97
+ cleanup();
98
+ process.exit(1);
99
+ });
100
+ server.listen(PORT, '127.0.0.1', () => {
101
+ console.error(`[EmbeddingServer] Listening on 127.0.0.1:${PORT}`);
102
+ fs.writeFileSync(PID_FILE, process.pid.toString());
103
+ });
104
+ process.on('SIGTERM', () => { console.error('[EmbeddingServer] SIGTERM received'); cleanup(); process.exit(0); });
105
+ process.on('SIGINT', () => { console.error('[EmbeddingServer] SIGINT received'); cleanup(); process.exit(0); });
106
+ }
107
+
108
+ startServer().catch((err) => { console.error('[EmbeddingServer] Failed to start:', err.message); process.exit(1); });
@@ -0,0 +1,421 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * LLM Server (Azure OpenAI) - LLM classification service using Azure OpenAI API
4
+ *
5
+ * Same functionality as llm-server.js, but uses Azure OpenAI instead of local llama-server
6
+ */
7
+
8
+ const net = require('net');
9
+ const https = require('https');
10
+ const fs = require('fs');
11
+ const path = require('path');
12
+ const os = require('os');
13
+ const config = require('../config');
14
+ const { ensureDir } = require('../lib/utils');
15
+
16
+ const PORT = config.llmPort;
17
+ const PID_FILE = path.join(config.pidDir, 'claude-llm.pid');
18
+
19
+ // Azure OpenAI Configuration
20
+ const AZURE_CONFIG = {
21
+ endpoint: config.azure.endpoint,
22
+ apiKey: config.azure.apiKey,
23
+ deployment: config.azure.deployment,
24
+ apiVersion: config.azure.apiVersion,
25
+ };
26
+
27
+ if (!AZURE_CONFIG.endpoint || !AZURE_CONFIG.apiKey) {
28
+ console.error('[LLMServer] Error: Azure OpenAI endpoint and apiKey must be configured.');
29
+ console.error('[LLMServer] Set AZURE_OPENAI_ENDPOINT and AZURE_OPENAI_KEY environment variables,');
30
+ console.error('[LLMServer] or configure them in config.js');
31
+ process.exit(1);
32
+ }
33
+
34
+ let isReady = false;
35
+ let server = null;
36
+
37
+ const LOG_FILE = path.join(config.logDir, 'llm-server.log');
38
+ ensureDir(config.logDir);
39
+
40
+ function str(v) { return v == null ? '' : typeof v === 'string' ? v : JSON.stringify(v); }
41
+
42
+ function log(msg) {
43
+ const line = `[${new Date().toISOString()}] ${msg}\n`;
44
+ try { fs.appendFileSync(LOG_FILE, line); } catch (e) {}
45
+ }
46
+
47
+ // ============== Azure OpenAI API ==============
48
+
49
+ async function callAzureOpenAI(messages, maxTokens = 200) {
50
+ const startTime = Date.now();
51
+ const userMsg = messages.find(m => m.role === 'user');
52
+ const sysMsg = messages.find(m => m.role === 'system');
53
+ log(`[AZURE-REQ] system=${str(sysMsg?.content)} user=${str(userMsg?.content)} max_tokens=${maxTokens}`);
54
+
55
+ return new Promise((resolve, reject) => {
56
+ const url = new URL(
57
+ `/openai/deployments/${AZURE_CONFIG.deployment}/chat/completions?api-version=${AZURE_CONFIG.apiVersion}`,
58
+ AZURE_CONFIG.endpoint
59
+ );
60
+
61
+ const postData = JSON.stringify({
62
+ messages,
63
+ max_tokens: maxTokens || 32768,
64
+ temperature: 0.1
65
+ });
66
+
67
+ const options = {
68
+ hostname: url.hostname,
69
+ port: 443,
70
+ path: url.pathname + url.search,
71
+ method: 'POST',
72
+ headers: {
73
+ 'Content-Type': 'application/json',
74
+ 'api-key': AZURE_CONFIG.apiKey
75
+ },
76
+ timeout: 60000
77
+ };
78
+
79
+ const req = https.request(options, (res) => {
80
+ let data = '';
81
+ res.on('data', chunk => data += chunk);
82
+ res.on('end', () => {
83
+ const duration = Date.now() - startTime;
84
+ try {
85
+ const json = JSON.parse(data);
86
+ if (json.error) {
87
+ log(`[AZURE-ERR] duration=${duration}ms error=${json.error.message || 'Azure API error'}`);
88
+ reject(new Error(json.error.message || 'Azure API error'));
89
+ } else {
90
+ const content = json.choices?.[0]?.message?.content || '';
91
+ const usage = json.usage || {};
92
+ log(`[AZURE-RES] duration=${duration}ms tokens=${usage.prompt_tokens||'-'}/${usage.completion_tokens||'-'}/${usage.total_tokens||'-'} response=${str(content)}`);
93
+ resolve(content);
94
+ }
95
+ } catch (e) {
96
+ log(`[AZURE-ERR] duration=${duration}ms parse_error=${e.message} raw=${str(data)}`);
97
+ reject(new Error(`Invalid response: ${data.slice(0, 200)}`));
98
+ }
99
+ });
100
+ });
101
+
102
+ req.on('error', (e) => {
103
+ const duration = Date.now() - startTime;
104
+ log(`[AZURE-ERR] duration=${duration}ms network_error=${e.message}`);
105
+ reject(e);
106
+ });
107
+ req.on('timeout', () => {
108
+ const duration = Date.now() - startTime;
109
+ log(`[AZURE-ERR] duration=${duration}ms timeout`);
110
+ req.destroy();
111
+ reject(new Error('Request timeout'));
112
+ });
113
+
114
+ req.write(postData);
115
+ req.end();
116
+ });
117
+ }
118
+
119
+ // ============== Request Handling ==============
120
+
121
+ async function handleRequest(data) {
122
+ const { action, text } = data;
123
+
124
+ switch (action) {
125
+ case 'ping':
126
+ return { success: true, ready: isReady };
127
+
128
+ case 'structurize': {
129
+ const { type: memType } = data;
130
+ const typeRules = {
131
+ fact: 'only <what>',
132
+ pattern: '<what> + <when> + <do> + <warn>',
133
+ decision: '<what> + <warn>',
134
+ preference: '<what> + <warn>',
135
+ bug: '<what> + <do>',
136
+ context: '<what> + <when>'
137
+ };
138
+ const rule = typeRules[memType] || typeRules.context;
139
+
140
+ const messages = [
141
+ {
142
+ role: 'system',
143
+ content: `You are a memory structuring assistant. Structure content into XML-formatted persistent memory.
144
+
145
+ First determine: is this worth saving long-term?
146
+ - One-off operation instructions ("change A to B") -> return REJECT
147
+ - Temporary conversation/debugging requests -> return REJECT
148
+ - Only meaningful in the current session -> return REJECT
149
+
150
+ If it has value, output XML (do not output anything else):
151
+ <memory type="${memType || 'context'}" domain="choose one: frontend/backend/database/devops/testing/memory/general">
152
+ <what>Core fact, 1-2 sentences, remove redundant words (required)</what>
153
+ <when>When to trigger/apply (use | to separate multiple scenarios)</when>
154
+ <do>Specific operation steps or commands (use ; to separate)</do>
155
+ <warn>Prohibited actions or common pitfalls</warn>
156
+ </memory>
157
+
158
+ Current type ${memType || 'context'} uses fields: ${rule}
159
+ Omit fields that are not needed.`
160
+ },
161
+ {
162
+ role: 'user',
163
+ content: text
164
+ }
165
+ ];
166
+
167
+ try {
168
+ const response = await callAzureOpenAI(messages, 300);
169
+ const trimmed = response.trim();
170
+
171
+ if (/REJECT/i.test(trimmed) && !trimmed.includes('<memory')) {
172
+ return { success: true, structured: { __rejected: true, reason: 'low value' } };
173
+ }
174
+
175
+ const xmlMatch = trimmed.match(/<memory[\s\S]*?<\/memory>/);
176
+ if (xmlMatch) {
177
+ return { success: true, structured: xmlMatch[0] };
178
+ }
179
+ return { success: false, error: 'No XML found in response' };
180
+ } catch (e) {
181
+ console.error('[LLMServer-Azure] Structurize error:', e.message);
182
+ return { success: false, error: e.message };
183
+ }
184
+ }
185
+
186
+ case 'merge': {
187
+ const { memories, domain: mergeDomain } = data;
188
+ if (!memories || !Array.isArray(memories) || memories.length === 0) {
189
+ return { success: false, error: 'memories array required' };
190
+ }
191
+
192
+ const d = mergeDomain || 'general';
193
+ const memoriesText = memories.map((m, i) => `[${i + 1}] ${m}`).join('\n');
194
+ const messages = [
195
+ {
196
+ role: 'system',
197
+ content: `You are a knowledge aggregation assistant. Deduplicate and merge multiple related memories into a single XML memory.
198
+
199
+ Merge rules:
200
+ - <what> Summarize the core theme of all memories in 1-2 sentences
201
+ - <when> Merge all applicable scenarios (use | to separate)
202
+ - <do> Merge all specific operations (use ; to separate), remove duplicates
203
+ - <warn> Merge all warnings, remove duplicates
204
+
205
+ Output only XML:
206
+ <memory type="pattern" domain="${d}">
207
+ <what>...</what>
208
+ <when>...</when>
209
+ <do>...</do>
210
+ <warn>...</warn>
211
+ </memory>`
212
+ },
213
+ {
214
+ role: 'user',
215
+ content: `Merge the following ${memories.length} memories:\n\n${memoriesText}`
216
+ }
217
+ ];
218
+
219
+ try {
220
+ const response = await callAzureOpenAI(messages, 500);
221
+ const xmlMatch = response.match(/<memory[\s\S]*?<\/memory>/);
222
+ if (xmlMatch) {
223
+ return { success: true, merged: xmlMatch[0] };
224
+ }
225
+
226
+ // fallback
227
+ return {
228
+ success: true,
229
+ merged: {
230
+ summary: memories[0].slice(0, 100),
231
+ content: memories.join('\n---\n'),
232
+ scenarios: [], rules: { must: [], must_not: [], prefer: [] }, triggers: []
233
+ }
234
+ };
235
+ } catch (e) {
236
+ console.error('[LLMServer-Azure] Merge error:', e.message);
237
+ return { success: false, error: e.message };
238
+ }
239
+ }
240
+
241
+ case 'analyzeSession': {
242
+ const { transcript } = data;
243
+ if (!transcript || transcript.length < 50) {
244
+ return { success: true, memories: [] };
245
+ }
246
+
247
+ const messages = [
248
+ {
249
+ role: 'system',
250
+ content: `You are a development session analysis assistant. Analyze session transcripts and extract content worth saving as long-term memory.
251
+
252
+ [Extraction criteria] Only extract:
253
+ 1. bug: error encountered -> fix experience (including error message and fix method)
254
+ 2. decision: user's explicitly stated technical decisions or preferences ("always use X from now on", "don't use Y")
255
+ 3. pattern: reusable development patterns or operational workflows
256
+ 4. preference: user's coding habits, tool preferences
257
+
258
+ [Do NOT extract]
259
+ - One-off operation instructions ("add a button", "modify the API", "optimize xxx")
260
+ - Code snapshots or specific implementation details (code changes, not worth memorizing)
261
+ - Routine file viewing/searching/installing dependencies/starting services
262
+ - Information queries and Q&A
263
+ - Specific steps of the current task
264
+
265
+ [Output format]
266
+ Return a <memory> block for each memory:
267
+ <memory type="choose one: bug/decision/pattern/preference" domain="choose one: frontend/backend/database/devops/testing/memory/general" confidence="0.7-0.9">
268
+ <summary>Plain text summary (one sentence)</summary>
269
+ <what>Core fact (1-2 sentences)</what>
270
+ <when>Trigger scenarios (optional, use | to separate)</when>
271
+ <do>Specific operations (optional, use ; to separate)</do>
272
+ <warn>Caveats (optional)</warn>
273
+ </memory>
274
+
275
+ If there is nothing worth saving, return only NONE.
276
+ Better to extract fewer items than to extract low-value content. Return at most 3 items.`
277
+ },
278
+ {
279
+ role: 'user',
280
+ content: `=== Session Transcript ===\n${transcript}`
281
+ }
282
+ ];
283
+
284
+ try {
285
+ const response = await callAzureOpenAI(messages, null);
286
+ const trimmed = response.trim();
287
+
288
+ if (/^NONE$/i.test(trimmed)) {
289
+ return { success: true, memories: [] };
290
+ }
291
+
292
+ // Extract all <memory> blocks
293
+ const memoryBlocks = [];
294
+ const regex = /<memory\s+([^>]+)>([\s\S]*?)<\/memory>/g;
295
+ let match;
296
+ while ((match = regex.exec(trimmed)) !== null) {
297
+ const attrs = match[1];
298
+ const body = match[2];
299
+
300
+ // Parse attributes
301
+ const type = (attrs.match(/type="([^"]+)"/) || [])[1] || 'pattern';
302
+ const domain = (attrs.match(/domain="([^"]+)"/) || [])[1] || 'general';
303
+ const confidence = parseFloat((attrs.match(/confidence="([^"]+)"/) || [])[1] || '0.8');
304
+
305
+ // Extract summary
306
+ const summaryMatch = body.match(/<summary>([\s\S]*?)<\/summary>/);
307
+ const summary = summaryMatch ? summaryMatch[1].trim() : '';
308
+
309
+ // Build structured_content (remove summary tag, keep the rest)
310
+ const structuredBody = body.replace(/<summary>[\s\S]*?<\/summary>\s*/, '');
311
+ const structuredContent = `<memory type="${type}" domain="${domain}">\n${structuredBody.trim()}\n</memory>`;
312
+
313
+ if (summary) {
314
+ memoryBlocks.push({ type, domain, confidence, summary, structuredContent });
315
+ }
316
+ }
317
+
318
+ return { success: true, memories: memoryBlocks };
319
+ } catch (e) {
320
+ console.error('[LLMServer-Azure] analyzeSession error:', e.message);
321
+ return { success: false, error: e.message, memories: [] };
322
+ }
323
+ }
324
+
325
+ default:
326
+ return { success: false, error: `Unknown action: ${action}` };
327
+ }
328
+ }
329
+
330
+ // ============== Server Startup ==============
331
+
332
+ async function checkAzureConnection() {
333
+ try {
334
+ const messages = [{ role: 'user', content: 'ping' }];
335
+ await callAzureOpenAI(messages, 10);
336
+ return true;
337
+ } catch (e) {
338
+ console.error('[LLMServer-Azure] Connection check failed:', e.message);
339
+ return false;
340
+ }
341
+ }
342
+
343
+ async function startServer() {
344
+ console.error('[LLMServer-Azure] Starting...');
345
+ console.error(`[LLMServer-Azure] Endpoint: ${AZURE_CONFIG.endpoint}`);
346
+ console.error(`[LLMServer-Azure] Deployment: ${AZURE_CONFIG.deployment}`);
347
+
348
+ // Check connection
349
+ const connected = await checkAzureConnection();
350
+ if (!connected) {
351
+ console.error('[LLMServer-Azure] Failed to connect to Azure OpenAI');
352
+ process.exit(1);
353
+ }
354
+ console.error('[LLMServer-Azure] Azure OpenAI connected');
355
+ isReady = true;
356
+
357
+ // Create TCP server
358
+ server = net.createServer((socket) => {
359
+ let buffer = '';
360
+
361
+ socket.on('data', async (chunk) => {
362
+ buffer += chunk.toString();
363
+ const lines = buffer.split('\n');
364
+ buffer = lines.pop();
365
+
366
+ for (const line of lines) {
367
+ if (line.trim()) {
368
+ try {
369
+ const data = JSON.parse(line);
370
+ const response = await handleRequest(data);
371
+ socket.write(JSON.stringify(response) + '\n');
372
+ } catch (e) {
373
+ socket.write(JSON.stringify({ success: false, error: e.message }) + '\n');
374
+ }
375
+ }
376
+ }
377
+ });
378
+
379
+ socket.on('error', () => {});
380
+ });
381
+
382
+ server.on('error', (err) => {
383
+ if (err.code === 'EADDRINUSE') {
384
+ console.error(`[LLMServer-Azure] Port ${PORT} already in use`);
385
+ process.exit(0);
386
+ }
387
+ console.error('[LLMServer-Azure] Server error:', err.message);
388
+ process.exit(1);
389
+ });
390
+
391
+ server.listen(PORT, '127.0.0.1', () => {
392
+ console.error(`[LLMServer-Azure] Listening on port ${PORT}`);
393
+ fs.writeFileSync(PID_FILE, process.pid.toString());
394
+ });
395
+
396
+ // Graceful shutdown
397
+ process.on('SIGTERM', () => {
398
+ console.error('[LLMServer-Azure] SIGTERM received');
399
+ cleanup();
400
+ process.exit(0);
401
+ });
402
+
403
+ process.on('SIGINT', () => {
404
+ console.error('[LLMServer-Azure] SIGINT received');
405
+ cleanup();
406
+ process.exit(0);
407
+ });
408
+ }
409
+
410
+ function cleanup() {
411
+ try {
412
+ if (fs.existsSync(PID_FILE)) {
413
+ fs.unlinkSync(PID_FILE);
414
+ }
415
+ } catch (e) {}
416
+ }
417
+
418
+ startServer().catch((err) => {
419
+ console.error('[LLMServer-Azure] Failed to start:', err.message);
420
+ process.exit(1);
421
+ });