@16pxh/cli-bridge 1.0.6 → 1.0.7

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/claude.mjs CHANGED
@@ -1,5 +1,8 @@
1
1
  import { spawn, execFile } from 'node:child_process';
2
2
  import { promisify } from 'node:util';
3
+ import { mkdirSync, writeFileSync, readFileSync, rmSync } from 'node:fs';
4
+ import { join } from 'node:path';
5
+ import { tmpdir } from 'node:os';
3
6
 
4
7
  const execFileAsync = promisify(execFile);
5
8
 
@@ -68,6 +71,86 @@ export function runPrompt(prompt, { sessionId } = {}) {
68
71
  });
69
72
  }
70
73
 
74
+ /**
75
+ * Write files to temp dir, run Claude CLI with edit permission, read back modified files.
76
+ * @param {string} prompt
77
+ * @param {{ html: string, css: string, js: string }} files
78
+ * @param {{ sessionId?: string }} [options]
79
+ * @returns {Promise<{ html: string, css: string, js: string, result: string, session_id: string, duration_ms: number }>}
80
+ */
81
+ export function runEditFiles(prompt, files, { sessionId } = {}) {
82
+ return new Promise((resolve, reject) => {
83
+ // Create temp dir with files
84
+ const tempDir = join(tmpdir(), `16pxh-edit-${Date.now()}`);
85
+ mkdirSync(tempDir, { recursive: true });
86
+ writeFileSync(join(tempDir, 'index.html'), files.html, 'utf-8');
87
+ writeFileSync(join(tempDir, 'style.css'), files.css, 'utf-8');
88
+ writeFileSync(join(tempDir, 'script.js'), files.js, 'utf-8');
89
+
90
+ const args = ['-p', prompt, '--output-format', 'json', '--allowedTools', 'Edit'];
91
+ if (sessionId) args.push('--resume', sessionId);
92
+
93
+ const child = spawn('claude', args, {
94
+ stdio: ['ignore', 'pipe', 'pipe'],
95
+ cwd: tempDir,
96
+ });
97
+
98
+ activeProcess = child;
99
+
100
+ let stdout = '';
101
+ let stderr = '';
102
+
103
+ child.stdout.on('data', (chunk) => { stdout += chunk; });
104
+ child.stderr.on('data', (chunk) => { stderr += chunk; });
105
+
106
+ child.on('close', (code) => {
107
+ if (activeProcess === child) activeProcess = null;
108
+
109
+ if (code !== 0) {
110
+ cleanup();
111
+ return reject(new Error(`Claude CLI exited with code ${code}: ${stderr.trim()}`));
112
+ }
113
+
114
+ let parsed;
115
+ try {
116
+ parsed = JSON.parse(stdout.trim());
117
+ } catch (err) {
118
+ cleanup();
119
+ return reject(new Error(`Failed to parse output: ${err.message}`));
120
+ }
121
+
122
+ // Read back modified files
123
+ try {
124
+ const html = readFileSync(join(tempDir, 'index.html'), 'utf-8');
125
+ const css = readFileSync(join(tempDir, 'style.css'), 'utf-8');
126
+ const js = readFileSync(join(tempDir, 'script.js'), 'utf-8');
127
+ cleanup();
128
+ resolve({
129
+ html,
130
+ css,
131
+ js,
132
+ result: parsed.result ?? '',
133
+ session_id: parsed.session_id ?? '',
134
+ duration_ms: parsed.duration_ms ?? 0,
135
+ });
136
+ } catch (err) {
137
+ cleanup();
138
+ reject(new Error(`Failed to read edited files: ${err.message}`));
139
+ }
140
+ });
141
+
142
+ child.on('error', (err) => {
143
+ if (activeProcess === child) activeProcess = null;
144
+ cleanup();
145
+ reject(err);
146
+ });
147
+
148
+ function cleanup() {
149
+ try { rmSync(tempDir, { recursive: true, force: true }); } catch {}
150
+ }
151
+ });
152
+ }
153
+
71
154
  /**
72
155
  * Kill the currently running claude child process, if any.
73
156
  */
package/lib/server.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import { createServer } from 'node:http';
2
2
  import { verifyToken, hasToken } from './auth.mjs';
3
- import { checkClaudeInstalled, runPrompt } from './claude.mjs';
3
+ import { checkClaudeInstalled, runPrompt, runEditFiles } from './claude.mjs';
4
4
  import { decryptSummary, encryptSummary } from './crypto.mjs';
5
5
 
6
6
  const PORT = 1676;
@@ -174,6 +174,58 @@ export function startServer() {
174
174
  return;
175
175
  }
176
176
 
177
+ // POST /edit-files — write html/css/js to temp files, let Claude edit, read back
178
+ if (req.method === 'POST' && url.pathname === '/edit-files') {
179
+ if (!authenticate(req, res)) {
180
+ logRequest('POST', '/edit-files', 403);
181
+ return;
182
+ }
183
+
184
+ let body;
185
+ try {
186
+ const raw = await readBody(req);
187
+ body = JSON.parse(raw);
188
+ } catch {
189
+ logRequest('POST', '/edit-files', 400, `${c.red}invalid JSON${c.reset}`);
190
+ json(res, 400, { error: 'Invalid JSON body' });
191
+ return;
192
+ }
193
+
194
+ const { prompt, html, css, js, session_id } = body;
195
+ if (!prompt) {
196
+ json(res, 400, { error: 'Missing prompt' });
197
+ return;
198
+ }
199
+
200
+ const promptPreview = truncateWords(prompt, 12);
201
+ console.log(`${c.dim}${timestamp()}${c.reset} ${c.magenta}→ edit${c.reset} ${c.dim}${promptPreview}${c.reset}`);
202
+
203
+ try {
204
+ const result = await runEditFiles(
205
+ prompt,
206
+ { html: html ?? '', css: css ?? '', js: js ?? '' },
207
+ { sessionId: session_id }
208
+ );
209
+
210
+ logRequest('POST', '/edit-files', 200, `${c.dim}${result.duration_ms}ms${c.reset}`);
211
+ console.log(`${c.dim}${timestamp()}${c.reset} ${c.green}← edit done${c.reset}`);
212
+
213
+ json(res, 200, {
214
+ success: true,
215
+ html: result.html,
216
+ css: result.css,
217
+ js: result.js,
218
+ result: result.result,
219
+ session_id: result.session_id,
220
+ duration_ms: result.duration_ms,
221
+ });
222
+ } catch (err) {
223
+ logRequest('POST', '/edit-files', 500, `${c.red}${err.message}${c.reset}`);
224
+ json(res, 500, { error: err.message });
225
+ }
226
+ return;
227
+ }
228
+
177
229
  // POST /summary — decrypt summary, run AI prompt, encrypt new summary
178
230
  if (req.method === 'POST' && url.pathname === '/summary') {
179
231
  if (!authenticate(req, res)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@16pxh/cli-bridge",
3
- "version": "1.0.6",
3
+ "version": "1.0.7",
4
4
  "description": "Bridge local Claude CLI to 16pxh AI features",
5
5
  "type": "module",
6
6
  "bin": {