@exreve/exk 1.0.55 → 1.0.56

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.
@@ -1,330 +0,0 @@
1
- import { v4 as uuidv4 } from 'uuid';
2
- import fs from 'fs/promises';
3
- import path from 'path';
4
- import { agentSessionManager } from './agentSession.js';
5
- const CONFIG_FILENAME = '.claude-voice.json';
6
- /**
7
- * Analyze project using Claude AI and generate structured JSON configuration
8
- */
9
- export async function analyzeProjectWithClaude(projectPath, projectName) {
10
- // Create a temporary session ID for analysis
11
- const analysisSessionId = `analysis-${uuidv4()}`;
12
- // Build the analysis prompt with structured output requirements
13
- const analysisPrompt = `Analyze the project structure at ${projectPath} and generate a comprehensive project configuration.
14
-
15
- CRITICAL: You must identify ALL applications/services in this project. Each app will get its own TypeScript runner file and entry in the JSON config.
16
-
17
- Please examine:
18
- 1. Project files and directory structure (use Read/Glob tools to explore)
19
- 2. Framework and technology stack (React, Express, FastAPI, Django, etc.)
20
- 3. Apps (applications/services) - each app can be at root or in subdirectories
21
- 4. For each app, identify:
22
- - App NAME: unique identifier for the app
23
- - App TYPE: "static-frontend" (React/Vue/Angular/Svelte/Next.js static builds) or "backend" (Express/Fastify/FastAPI/Django/etc.)
24
- - How to BUILD it (buildCommand - REQUIRED for static frontends)
25
- - How to START it (startCommand - REQUIRED for all apps)
26
- - How to STOP it (stopCommand - optional, defaults to killing process)
27
- - How to RESTART it (restartCommand - optional, defaults to stop + start)
28
- - Port number (port - REQUIRED for HTTP apps)
29
- - Protocol (http/https/ws/etc.)
30
- - HTTP endpoints/routes (endpoints array)
31
- - Framework name (react/express/fastify/etc.)
32
- - Directory location (relative to project root, empty string "" for root app)
33
- - Build output directory (buildDir - for static frontends: dist/build/public)
34
- - Environment variables (env object)
35
- - Health check URL or command
36
- 5. Check package.json scripts, Makefile, docker-compose.yml, Procfile, vite.config.js, next.config.js, etc. for commands
37
- 6. Detect HTTP endpoints by examining route files, controllers, or API definitions
38
- 7. For static frontends, identify the build output directory (usually dist, build, or public)
39
- 8. Look for multiple apps in monorepos or multi-app projects
40
-
41
- IMPORTANT OUTPUT REQUIREMENTS:
42
- - You must output a JSON file named ".talk-to-code.json" in the project root
43
- - The JSON must contain ALL apps/services found in the project
44
- - Each app will be wrapped with a TypeScript runner file (generated automatically)
45
- - Apps must be configured to work with the runner system
46
-
47
- Output ONLY valid JSON matching this exact structure (no markdown, no code blocks, no explanations, just pure JSON):
48
-
49
- {
50
- "version": "1.0.0",
51
- "projectName": "${projectName}",
52
- "description": "Description of the project",
53
- "apps": [
54
- {
55
- "name": "app-name",
56
- "description": "Optional description",
57
- "type": "http",
58
- "port": 3000,
59
- "protocol": "http",
60
- "directory": "relative/path/to/app",
61
- "framework": "react",
62
- "appType": "static-frontend",
63
- "buildDir": "dist",
64
- "endpoints": ["/", "/api", "/api/v1"],
65
- "env": {},
66
- "healthCheck": {
67
- "url": "http://localhost:3000/health"
68
- },
69
- "buildCommand": "npm run build",
70
- "startCommand": "npm start",
71
- "stopCommand": "pkill -f 'node.*start'",
72
- "restartCommand": null,
73
- "dependencies": []
74
- }
75
- ],
76
- "directories": ["subdirectory1", "subdirectory2"],
77
- "metadata": {
78
- "analyzedAt": "${new Date().toISOString()}",
79
- "analyzedBy": "talk-to-code-agent",
80
- "framework": "detected-framework",
81
- "language": "javascript",
82
- "packageManager": "npm"
83
- }
84
- }
85
-
86
- CRITICAL INSTRUCTIONS:
87
- 1. Output ONLY the JSON object, nothing else
88
- 2. No markdown formatting, no code blocks, no explanations
89
- 3. Use Read/Glob tools to examine the project structure thoroughly
90
- 4. Extract actual port numbers, commands, and configurations from files
91
- 5. startCommand is REQUIRED for each app - must be the actual command to start the app
92
- 6. stopCommand and restartCommand are optional (can be null)
93
- 7. appType MUST be "static-frontend" for React/Vue/Angular/Svelte/Next.js static builds, or "backend" for Express/Fastify/FastAPI/Django/etc.
94
- 8. buildDir MUST be specified for static frontends (usually "dist", "build", or "public")
95
- 9. port is REQUIRED for HTTP apps
96
- 10. endpoints should be an array of HTTP routes (e.g., ["/", "/api", "/api/v1"])
97
- 11. directory should be relative path from project root, empty string "" for root app
98
- 12. Be thorough and accurate - identify ALL apps/services in the project
99
- 13. Each app will get a TypeScript runner file ({appName}_runner.ts) generated automatically
100
- 14. The runner will wrap the app to collect logs, stats, and provide unified control
101
- 15. After generating the JSON, write it to ".claude-voice.json" file in the project root using the Write tool`;
102
- let config = null;
103
- let lastAssistantMessage = '';
104
- let errorOccurred = false;
105
- let foundJson = false;
106
- const assistantMessages = [];
107
- try {
108
- // Create temporary session handler
109
- await agentSessionManager.createSession({
110
- sessionId: analysisSessionId,
111
- projectPath,
112
- onOutput: (output) => {
113
- // Capture assistant messages to extract JSON
114
- if (output.type === 'assistant') {
115
- const data = typeof output.data === 'string' ? output.data : JSON.stringify(output.data);
116
- assistantMessages.push(data);
117
- lastAssistantMessage = data;
118
- }
119
- // Check for file writes (Claude might write the JSON file directly)
120
- if (output.type === 'tool_result' && output.metadata?.toolName === 'Write') {
121
- const toolResult = output.metadata.toolResult;
122
- if (toolResult && typeof toolResult === 'object' && 'file_path' in toolResult) {
123
- const filePath = toolResult.file_path;
124
- if (filePath && filePath.includes('.talk-to-code.json')) {
125
- // Claude wrote the config file, mark as found
126
- foundJson = true;
127
- }
128
- }
129
- }
130
- if (output.type === 'result' && output.metadata?.isError) {
131
- errorOccurred = true;
132
- }
133
- },
134
- onError: (error) => {
135
- console.error(`Analysis session error: ${error}`);
136
- errorOccurred = true;
137
- },
138
- onComplete: () => {
139
- // Session completed
140
- }
141
- });
142
- // Send analysis prompt
143
- await agentSessionManager.sendPrompt(analysisSessionId, analysisPrompt, [], {
144
- sessionId: analysisSessionId,
145
- projectPath,
146
- onOutput: (output) => {
147
- // Capture assistant messages
148
- if (output.type === 'assistant') {
149
- const data = typeof output.data === 'string' ? output.data : JSON.stringify(output.data);
150
- assistantMessages.push(data);
151
- lastAssistantMessage = data;
152
- }
153
- // Also check for file writes (Claude might write the JSON file directly)
154
- if (output.type === 'tool_result' && output.metadata?.toolName === 'Write') {
155
- // Claude wrote a file, check if it's the config file
156
- const toolResult = output.metadata.toolResult;
157
- if (toolResult && typeof toolResult === 'object' && 'file_path' in toolResult) {
158
- const filePath = toolResult.file_path;
159
- if (filePath && filePath.includes('.talk-to-code.json')) {
160
- // Claude wrote the config file, try to read it
161
- setTimeout(async () => {
162
- try {
163
- const configPath = path.join(projectPath, '.talk-to-code.json');
164
- const content = await fs.readFile(configPath, 'utf-8');
165
- const parsed = JSON.parse(content);
166
- if (parsed && parsed.apps) {
167
- config = parsed;
168
- foundJson = true;
169
- }
170
- }
171
- catch (error) {
172
- // Ignore read errors
173
- }
174
- }, 1000);
175
- }
176
- }
177
- }
178
- },
179
- onError: (error) => {
180
- console.error(`Analysis error: ${error}`);
181
- errorOccurred = true;
182
- },
183
- onComplete: () => {
184
- // Analysis complete
185
- }
186
- });
187
- // Wait for Claude to process (give it time to read files and generate response)
188
- // Use exponential backoff: start at 500ms, double each time, cap at 5s, timeout at 90s
189
- const configFilePath = path.join(projectPath, CONFIG_FILENAME);
190
- const POLL_TIMEOUT_MS = 90_000;
191
- const POLL_START_MS = 500;
192
- const POLL_MAX_MS = 5_000;
193
- let elapsed = 0;
194
- let interval = POLL_START_MS;
195
- while (elapsed < POLL_TIMEOUT_MS) {
196
- await new Promise(resolve => setTimeout(resolve, interval));
197
- elapsed += interval;
198
- interval = Math.min(interval * 2, POLL_MAX_MS);
199
- // Check if Claude wrote the config file directly
200
- try {
201
- await fs.access(configFilePath);
202
- const fileContent = await fs.readFile(configFilePath, 'utf-8');
203
- const parsed = JSON.parse(fileContent);
204
- if (parsed && parsed.apps) {
205
- config = parsed;
206
- foundJson = true;
207
- break;
208
- }
209
- }
210
- catch {
211
- // File doesn't exist yet or invalid JSON, continue waiting
212
- }
213
- // Check if we have a complete JSON response in messages
214
- if (lastAssistantMessage && (lastAssistantMessage.includes('"version"') || lastAssistantMessage.includes('"apps"') || lastAssistantMessage.includes('"services"'))) {
215
- foundJson = true;
216
- break;
217
- }
218
- // Check if session completed (result message received)
219
- if (errorOccurred) {
220
- break;
221
- }
222
- }
223
- // If config was read from file, use it instead of parsing messages
224
- if (!config || !foundJson) {
225
- // Try to extract JSON from the assistant messages (check all messages, latest first)
226
- for (let i = assistantMessages.length - 1; i >= 0; i--) {
227
- let jsonStr = assistantMessages[i].trim();
228
- // Remove markdown code blocks if present
229
- jsonStr = jsonStr.replace(/^```json\s*/i, '').replace(/^```\s*/, '').replace(/\s*```$/, '');
230
- jsonStr = jsonStr.trim();
231
- // Try to extract JSON object if wrapped in text
232
- const jsonMatch = jsonStr.match(/\{[\s\S]*\}/);
233
- if (jsonMatch) {
234
- jsonStr = jsonMatch[0];
235
- }
236
- try {
237
- const parsedConfig = JSON.parse(jsonStr);
238
- config = parsedConfig;
239
- foundJson = true;
240
- break;
241
- }
242
- catch (parseError) {
243
- // Try next message
244
- continue;
245
- }
246
- }
247
- }
248
- // Validate and ensure required fields if config was loaded
249
- if (config && foundJson) {
250
- // Validate and ensure required fields
251
- if (!config.version)
252
- config.version = '1.0.0';
253
- if (!config.projectName)
254
- config.projectName = projectName;
255
- if (!config.apps)
256
- config.apps = [];
257
- if (!config.directories)
258
- config.directories = [];
259
- // Migrate old "services" to "apps" if present
260
- if (config.services && !config.apps) {
261
- config.apps = config.services;
262
- }
263
- // Ensure each app has a startCommand
264
- config.apps = config.apps.map(app => ({
265
- ...app,
266
- startCommand: app.startCommand || 'echo "No start command configured"',
267
- }));
268
- if (!config.metadata) {
269
- config.metadata = {
270
- analyzedAt: new Date().toISOString(),
271
- analyzedBy: 'talk-to-code-agent'
272
- };
273
- }
274
- else {
275
- config.metadata.analyzedAt = new Date().toISOString();
276
- config.metadata.analyzedBy = 'talk-to-code-agent';
277
- }
278
- }
279
- // Clean up session
280
- try {
281
- await agentSessionManager.deleteSession(analysisSessionId);
282
- }
283
- catch (cleanupError) {
284
- console.warn(`Failed to cleanup analysis session:`, cleanupError);
285
- }
286
- }
287
- catch (error) {
288
- console.error(`Error during Claude analysis:`, error.message);
289
- errorOccurred = true;
290
- // Try to cleanup session
291
- try {
292
- await agentSessionManager.deleteSession(analysisSessionId);
293
- }
294
- catch { }
295
- }
296
- // If analysis failed or no config extracted, throw error (caller can handle fallback)
297
- if (!config || errorOccurred) {
298
- throw new Error(`Claude analysis failed or incomplete. Last message: ${lastAssistantMessage.substring(0, 200)}`);
299
- }
300
- return config;
301
- }
302
- /**
303
- * Read project configuration from file
304
- */
305
- export async function getProjectConfig(projectPath) {
306
- try {
307
- const configPath = path.join(projectPath, CONFIG_FILENAME);
308
- const content = await fs.readFile(configPath, 'utf-8');
309
- const config = JSON.parse(content);
310
- // Migrate old "services" to "apps" if present
311
- if (config.services && !config.apps) {
312
- config.apps = config.services;
313
- }
314
- return config;
315
- }
316
- catch (error) {
317
- if (error.code === 'ENOENT') {
318
- return null; // Config file doesn't exist yet
319
- }
320
- throw error;
321
- }
322
- }
323
- /**
324
- * Save project configuration to file
325
- */
326
- export async function saveProjectConfig(projectPath, config) {
327
- const configPath = path.join(projectPath, CONFIG_FILENAME);
328
- const content = JSON.stringify(config, null, 2);
329
- await fs.writeFile(configPath, content, 'utf-8');
330
- }
@@ -1,69 +0,0 @@
1
- import fs from 'fs/promises';
2
- import path from 'path';
3
- const toAbsolutePath = (p) => path.isAbsolute(p) ? p : path.resolve(p);
4
- export async function createProject(request) {
5
- try {
6
- const { path: projectPath, sourcePath } = request;
7
- const absolutePath = toAbsolutePath(projectPath);
8
- // Check if source path exists (if linking)
9
- if (sourcePath) {
10
- const absoluteSourcePath = toAbsolutePath(sourcePath);
11
- try {
12
- const sourceStat = await fs.stat(absoluteSourcePath);
13
- if (!sourceStat.isDirectory()) {
14
- return { success: false, error: 'Source path is not a directory' };
15
- }
16
- }
17
- catch {
18
- return { success: false, error: 'Source path does not exist' };
19
- }
20
- }
21
- // Check if project directory already exists
22
- try {
23
- const stat = await fs.stat(absolutePath);
24
- if (!stat.isDirectory()) {
25
- return { success: false, error: 'Path exists but is not a directory' };
26
- }
27
- }
28
- catch {
29
- // Directory doesn't exist, create it
30
- try {
31
- await fs.mkdir(absolutePath, { recursive: true });
32
- }
33
- catch (error) {
34
- return { success: false, error: `Failed to create directory: ${error.message}` };
35
- }
36
- }
37
- // If linking to source, create symlink or copy (for now, just validate)
38
- // In a real implementation, you might want to create a symlink or copy files
39
- if (sourcePath) {
40
- // For now, we just validate that both paths exist
41
- // The actual linking can be done by the application logic
42
- }
43
- return { success: true, actualPath: absolutePath };
44
- }
45
- catch (error) {
46
- return { success: false, error: error.message };
47
- }
48
- }
49
- export async function deleteProject(projectPath) {
50
- try {
51
- const absolutePath = toAbsolutePath(projectPath);
52
- try {
53
- const stat = await fs.stat(absolutePath);
54
- if (!stat.isDirectory()) {
55
- return { success: false, error: 'Path is not a directory' };
56
- }
57
- }
58
- catch {
59
- // Directory doesn't exist, consider it deleted
60
- return { success: true };
61
- }
62
- // Don't delete the directory on device - just succeed
63
- // The directory will remain on the filesystem
64
- return { success: true };
65
- }
66
- catch (error) {
67
- return { success: false, error: error.message };
68
- }
69
- }
@@ -1,210 +0,0 @@
1
- /**
2
- * Generate TypeScript runner code for an app
3
- */
4
- export function generateRunnerCode(app, projectPath) {
5
- const appType = app.appType || (app.framework?.toLowerCase().includes('react') ||
6
- app.framework?.toLowerCase().includes('vue') ||
7
- app.framework?.toLowerCase().includes('angular') ||
8
- app.framework?.toLowerCase().includes('svelte') ? 'static-frontend' : 'backend');
9
- if (appType === 'static-frontend') {
10
- return generateStaticFrontendRunner(app, projectPath);
11
- }
12
- else {
13
- return generateBackendRunner(app, projectPath);
14
- }
15
- }
16
- function generateStaticFrontendRunner(app, _projectPath) {
17
- const appName = app.name.replace(/[^a-zA-Z0-9_-]/g, '_'); // Sanitize app name for filename
18
- const port = app.port || 3000;
19
- const buildDir = app.buildDir || 'dist';
20
- const appDir = app.directory || '';
21
- return `import Fastify from 'fastify'
22
- import fastifyStatic from '@fastify/static'
23
- import path from 'path'
24
- import fs from 'fs/promises'
25
- import { fileURLToPath } from 'url'
26
- import { dirname } from 'path'
27
-
28
- const __filename = fileURLToPath(import.meta.url)
29
- const __dirname = dirname(__filename)
30
- const PORT = ${port}
31
- const BUILD_DIR = path.join(__dirname, ${appDir ? `'${appDir}', ` : ''}'${buildDir}')
32
-
33
- async function start() {
34
- // Check if build directory exists
35
- try {
36
- await fs.access(BUILD_DIR)
37
- } catch {
38
- console.error(\`✗ Build directory not found: \${BUILD_DIR}\`)
39
- console.error(' Please build the project first using: ' + (process.env.BUILD_COMMAND || 'npm run build'))
40
- process.exit(1)
41
- }
42
-
43
- const fastify = Fastify({
44
- logger: {
45
- level: 'info',
46
- transport: {
47
- target: 'pino-pretty',
48
- options: {
49
- translateTime: 'HH:MM:ss Z',
50
- ignore: 'pid,hostname',
51
- },
52
- },
53
- },
54
- })
55
-
56
- // Register static file serving
57
- await fastify.register(fastifyStatic, {
58
- root: BUILD_DIR,
59
- prefix: '/',
60
- })
61
-
62
- // Request logging
63
- fastify.addHook('onRequest', async (request, reply) => {
64
- const logLine = \`[\${new Date().toISOString()}] \${request.method} \${request.url} - \${reply.statusCode}\\n\`
65
- await fs.appendFile('${appName}_runner.log', logLine, 'utf-8').catch(() => {})
66
- })
67
-
68
- // Error handling
69
- fastify.setErrorHandler(async (error, request, reply) => {
70
- const logLine = \`[\${new Date().toISOString()}] ERROR: \${error.message} - \${request.method} \${request.url}\\n\`
71
- await fs.appendFile('${appName}_runner.log', logLine, 'utf-8').catch(() => {})
72
- reply.status(500).send({ error: 'Internal Server Error' })
73
- })
74
-
75
- try {
76
- await fastify.listen({ port: PORT, host: '0.0.0.0' })
77
- console.log(\`✓ Static server started on port \${PORT}\`)
78
- console.log(\` Serving from: \${BUILD_DIR}\`)
79
- } catch (error: any) {
80
- console.error(\`✗ Failed to start server:\`, error.message)
81
- process.exit(1)
82
- }
83
- }
84
-
85
- // Handle graceful shutdown
86
- process.on('SIGTERM', async () => {
87
- console.log('Shutting down...')
88
- await fastify.close()
89
- process.exit(0)
90
- })
91
-
92
- process.on('SIGINT', async () => {
93
- console.log('Shutting down...')
94
- await fastify.close()
95
- process.exit(0)
96
- })
97
-
98
- start()
99
- `;
100
- }
101
- function generateBackendRunner(app, _projectPath) {
102
- const appName = app.name.replace(/[^a-zA-Z0-9_-]/g, '_'); // Sanitize app name for filename
103
- const appDir = app.directory || '';
104
- const startCommand = app.startCommand;
105
- const envVars = app.env || {};
106
- const envString = Object.entries(envVars)
107
- .map(([key, value]) => ` ${key}: '${value}',`)
108
- .join('\n');
109
- return `import { spawn } from 'child_process'
110
- import fs from 'fs/promises'
111
- import path from 'path'
112
- import { fileURLToPath } from 'url'
113
- import { dirname } from 'path'
114
-
115
- const __filename = fileURLToPath(import.meta.url)
116
- const __dirname = dirname(__filename)
117
- const APP_DIR = path.join(__dirname, ${appDir ? `'${appDir}'` : 'undefined'})
118
- const START_COMMAND = '${startCommand}'
119
-
120
- const env = {
121
- ...process.env,
122
- ${envString}
123
- NODE_ENV: process.env.NODE_ENV || 'development',
124
- }
125
-
126
- let childProcess: ReturnType<typeof spawn> | null = null
127
-
128
- async function start() {
129
- const workingDir = APP_DIR || process.cwd()
130
-
131
- console.log(\`Starting backend app: ${app.name}\`)
132
- console.log(\` Directory: \${workingDir}\`)
133
- console.log(\` Command: \${START_COMMAND}\`)
134
-
135
- const isWin = process.platform === 'win32'
136
- const shell = isWin ? (process.env.COMSPEC || 'cmd.exe') : 'sh'
137
- const args = isWin ? ['/c', START_COMMAND] : ['-c', START_COMMAND]
138
- childProcess = spawn(shell, args, {
139
- cwd: workingDir,
140
- env,
141
- stdio: ['ignore', 'pipe', 'pipe'],
142
- })
143
-
144
- if (!childProcess.pid) {
145
- console.error('✗ Failed to spawn process')
146
- process.exit(1)
147
- }
148
-
149
- console.log(\`✓ Backend started with PID: \${childProcess.pid}\`)
150
-
151
- // Capture stdout
152
- childProcess.stdout?.on('data', async (data: Buffer) => {
153
- const output = data.toString()
154
- const logLine = \`[\${new Date().toISOString()}] [STDOUT] \${output}\`
155
- await fs.appendFile('${appName}_runner.log', logLine, 'utf-8').catch(() => {})
156
- process.stdout.write(output)
157
- })
158
-
159
- // Capture stderr
160
- childProcess.stderr?.on('data', async (data: Buffer) => {
161
- const output = data.toString()
162
- const logLine = \`[\${new Date().toISOString()}] [STDERR] \${output}\`
163
- await fs.appendFile('${appName}_runner.log', logLine, 'utf-8').catch(() => {})
164
- process.stderr.write(output)
165
- })
166
-
167
- // Handle process exit
168
- childProcess.on('exit', async (code) => {
169
- const logLine = \`[\${new Date().toISOString()}] Process exited with code \${code}\\n\`
170
- await fs.appendFile('${appName}_runner.log', logLine, 'utf-8').catch(() => {})
171
- console.log(\`Process exited with code \${code}\`)
172
- childProcess = null
173
- })
174
-
175
- // Handle process errors
176
- childProcess.on('error', async (error) => {
177
- const logLine = \`[\${new Date().toISOString()}] Process error: \${error.message}\\n\`
178
- await fs.appendFile('${appName}_runner.log', logLine, 'utf-8').catch(() => {})
179
- console.error(\`Process error: \${error.message}\`)
180
- })
181
- }
182
-
183
- // Handle graceful shutdown
184
- process.on('SIGTERM', async () => {
185
- console.log('Shutting down...')
186
- if (childProcess) {
187
- childProcess.kill('SIGTERM')
188
- await new Promise(resolve => setTimeout(resolve, 2000))
189
- if (childProcess) {
190
- childProcess.kill('SIGKILL')
191
- }
192
- }
193
- process.exit(0)
194
- })
195
-
196
- process.on('SIGINT', async () => {
197
- console.log('Shutting down...')
198
- if (childProcess) {
199
- childProcess.kill('SIGTERM')
200
- await new Promise(resolve => setTimeout(resolve, 2000))
201
- if (childProcess) {
202
- childProcess.kill('SIGKILL')
203
- }
204
- }
205
- process.exit(0)
206
- })
207
-
208
- start()
209
- `;
210
- }