@16pxh/cli-bridge 1.0.6 → 1.1.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/claude.mjs +100 -0
- package/lib/server.mjs +68 -2
- package/package.json +1 -1
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
|
|
|
@@ -42,6 +45,12 @@ export function runPrompt(prompt, { sessionId } = {}) {
|
|
|
42
45
|
child.on('close', (code) => {
|
|
43
46
|
if (activeProcess === child) activeProcess = null;
|
|
44
47
|
|
|
48
|
+
// Retry without session if session expired
|
|
49
|
+
if (code !== 0 && sessionId && stderr.includes('No conversation found')) {
|
|
50
|
+
resolve(runPrompt(prompt, {}));
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
45
54
|
if (code !== 0) {
|
|
46
55
|
return reject(new Error(`Claude CLI exited with code ${code}: ${stderr.trim()}`));
|
|
47
56
|
}
|
|
@@ -57,6 +66,8 @@ export function runPrompt(prompt, { sessionId } = {}) {
|
|
|
57
66
|
result: parsed.result ?? parsed.content ?? '',
|
|
58
67
|
session_id: parsed.session_id ?? '',
|
|
59
68
|
duration_ms: parsed.duration_ms ?? 0,
|
|
69
|
+
usage: parsed.usage ?? null,
|
|
70
|
+
cost_usd: parsed.total_cost_usd ?? 0,
|
|
60
71
|
});
|
|
61
72
|
});
|
|
62
73
|
|
|
@@ -68,6 +79,95 @@ export function runPrompt(prompt, { sessionId } = {}) {
|
|
|
68
79
|
});
|
|
69
80
|
}
|
|
70
81
|
|
|
82
|
+
/**
|
|
83
|
+
* Write files to temp dir, run Claude CLI with edit permission, read back modified files.
|
|
84
|
+
* @param {string} prompt
|
|
85
|
+
* @param {{ html: string, css: string, js: string }} files
|
|
86
|
+
* @param {{ sessionId?: string }} [options]
|
|
87
|
+
* @returns {Promise<{ html: string, css: string, js: string, result: string, session_id: string, duration_ms: number }>}
|
|
88
|
+
*/
|
|
89
|
+
export function runEditFiles(prompt, files, { sessionId } = {}) {
|
|
90
|
+
return new Promise((resolve, reject) => {
|
|
91
|
+
// Create temp dir with files
|
|
92
|
+
const tempDir = join(tmpdir(), `16pxh-edit-${Date.now()}`);
|
|
93
|
+
mkdirSync(tempDir, { recursive: true });
|
|
94
|
+
writeFileSync(join(tempDir, 'index.html'), files.html, 'utf-8');
|
|
95
|
+
writeFileSync(join(tempDir, 'style.css'), files.css, 'utf-8');
|
|
96
|
+
writeFileSync(join(tempDir, 'script.js'), files.js, 'utf-8');
|
|
97
|
+
|
|
98
|
+
const args = ['-p', prompt, '--output-format', 'json', '--allowedTools', 'Edit'];
|
|
99
|
+
if (sessionId) args.push('--resume', sessionId);
|
|
100
|
+
|
|
101
|
+
const child = spawn('claude', args, {
|
|
102
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
103
|
+
cwd: tempDir,
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
activeProcess = child;
|
|
107
|
+
|
|
108
|
+
let stdout = '';
|
|
109
|
+
let stderr = '';
|
|
110
|
+
|
|
111
|
+
child.stdout.on('data', (chunk) => { stdout += chunk; });
|
|
112
|
+
child.stderr.on('data', (chunk) => { stderr += chunk; });
|
|
113
|
+
|
|
114
|
+
child.on('close', (code) => {
|
|
115
|
+
if (activeProcess === child) activeProcess = null;
|
|
116
|
+
|
|
117
|
+
// Retry without session if session expired
|
|
118
|
+
if (code !== 0 && sessionId && stderr.includes('No conversation found')) {
|
|
119
|
+
cleanup();
|
|
120
|
+
resolve(runEditFiles(prompt, files, {}));
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (code !== 0) {
|
|
125
|
+
cleanup();
|
|
126
|
+
return reject(new Error(`Claude CLI exited with code ${code}: ${stderr.trim()}`));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
let parsed;
|
|
130
|
+
try {
|
|
131
|
+
parsed = JSON.parse(stdout.trim());
|
|
132
|
+
} catch (err) {
|
|
133
|
+
cleanup();
|
|
134
|
+
return reject(new Error(`Failed to parse output: ${err.message}`));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Read back modified files
|
|
138
|
+
try {
|
|
139
|
+
const html = readFileSync(join(tempDir, 'index.html'), 'utf-8');
|
|
140
|
+
const css = readFileSync(join(tempDir, 'style.css'), 'utf-8');
|
|
141
|
+
const js = readFileSync(join(tempDir, 'script.js'), 'utf-8');
|
|
142
|
+
cleanup();
|
|
143
|
+
resolve({
|
|
144
|
+
html,
|
|
145
|
+
css,
|
|
146
|
+
js,
|
|
147
|
+
result: parsed.result ?? '',
|
|
148
|
+
session_id: parsed.session_id ?? '',
|
|
149
|
+
duration_ms: parsed.duration_ms ?? 0,
|
|
150
|
+
usage: parsed.usage ?? null,
|
|
151
|
+
cost_usd: parsed.total_cost_usd ?? 0,
|
|
152
|
+
});
|
|
153
|
+
} catch (err) {
|
|
154
|
+
cleanup();
|
|
155
|
+
reject(new Error(`Failed to read edited files: ${err.message}`));
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
child.on('error', (err) => {
|
|
160
|
+
if (activeProcess === child) activeProcess = null;
|
|
161
|
+
cleanup();
|
|
162
|
+
reject(err);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
function cleanup() {
|
|
166
|
+
try { rmSync(tempDir, { recursive: true, force: true }); } catch {}
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
71
171
|
/**
|
|
72
172
|
* Kill the currently running claude child process, if any.
|
|
73
173
|
*/
|
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;
|
|
@@ -40,6 +40,18 @@ function logRequest(method, path, status, extra = '') {
|
|
|
40
40
|
);
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
function logTokens(usage, costUsd) {
|
|
44
|
+
if (!usage) return;
|
|
45
|
+
const input = usage.input_tokens ?? 0;
|
|
46
|
+
const output = usage.output_tokens ?? 0;
|
|
47
|
+
const cacheRead = usage.cache_read_input_tokens ?? 0;
|
|
48
|
+
const cacheCreate = usage.cache_creation_input_tokens ?? 0;
|
|
49
|
+
const cost = typeof costUsd === 'number' ? `$${costUsd.toFixed(4)}` : '';
|
|
50
|
+
console.log(
|
|
51
|
+
`${c.dim}${timestamp()}${c.reset} ${c.dim}tokens: in=${input} out=${output} cache_read=${cacheRead} cache_create=${cacheCreate} ${cost}${c.reset}`
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
43
55
|
const CORS_HEADERS = {
|
|
44
56
|
'Access-Control-Allow-Origin': '*',
|
|
45
57
|
'Access-Control-Allow-Headers': 'x-bridge-token, content-type',
|
|
@@ -162,9 +174,10 @@ export function startServer() {
|
|
|
162
174
|
}
|
|
163
175
|
|
|
164
176
|
try {
|
|
165
|
-
const { result, session_id: responseSessionId, duration_ms } = await runPrompt(fullPrompt, { sessionId: session_id });
|
|
177
|
+
const { result, session_id: responseSessionId, duration_ms, usage, cost_usd } = await runPrompt(fullPrompt, { sessionId: session_id });
|
|
166
178
|
const resultPreview = truncateChars(result, 64);
|
|
167
179
|
logRequest('POST', '/prompt', 200, `${c.dim}${duration_ms}ms${c.reset}`);
|
|
180
|
+
logTokens(usage, cost_usd);
|
|
168
181
|
console.log(`${c.dim}${timestamp()}${c.reset} ${c.green}←${c.reset} ${c.dim}${resultPreview}${c.reset}`);
|
|
169
182
|
json(res, 200, { success: true, result, session_id: responseSessionId, duration_ms });
|
|
170
183
|
} catch (err) {
|
|
@@ -174,6 +187,59 @@ export function startServer() {
|
|
|
174
187
|
return;
|
|
175
188
|
}
|
|
176
189
|
|
|
190
|
+
// POST /edit-files — write html/css/js to temp files, let Claude edit, read back
|
|
191
|
+
if (req.method === 'POST' && url.pathname === '/edit-files') {
|
|
192
|
+
if (!authenticate(req, res)) {
|
|
193
|
+
logRequest('POST', '/edit-files', 403);
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
let body;
|
|
198
|
+
try {
|
|
199
|
+
const raw = await readBody(req);
|
|
200
|
+
body = JSON.parse(raw);
|
|
201
|
+
} catch {
|
|
202
|
+
logRequest('POST', '/edit-files', 400, `${c.red}invalid JSON${c.reset}`);
|
|
203
|
+
json(res, 400, { error: 'Invalid JSON body' });
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const { prompt, html, css, js, session_id } = body;
|
|
208
|
+
if (!prompt) {
|
|
209
|
+
json(res, 400, { error: 'Missing prompt' });
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const promptPreview = truncateWords(prompt, 12);
|
|
214
|
+
console.log(`${c.dim}${timestamp()}${c.reset} ${c.magenta}→ edit${c.reset} ${c.dim}${promptPreview}${c.reset}`);
|
|
215
|
+
|
|
216
|
+
try {
|
|
217
|
+
const result = await runEditFiles(
|
|
218
|
+
prompt,
|
|
219
|
+
{ html: html ?? '', css: css ?? '', js: js ?? '' },
|
|
220
|
+
{ sessionId: session_id }
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
logRequest('POST', '/edit-files', 200, `${c.dim}${result.duration_ms}ms${c.reset}`);
|
|
224
|
+
logTokens(result.usage, result.cost_usd);
|
|
225
|
+
console.log(`${c.dim}${timestamp()}${c.reset} ${c.green}← edit done${c.reset}`);
|
|
226
|
+
|
|
227
|
+
json(res, 200, {
|
|
228
|
+
success: true,
|
|
229
|
+
html: result.html,
|
|
230
|
+
css: result.css,
|
|
231
|
+
js: result.js,
|
|
232
|
+
result: result.result,
|
|
233
|
+
session_id: result.session_id,
|
|
234
|
+
duration_ms: result.duration_ms,
|
|
235
|
+
});
|
|
236
|
+
} catch (err) {
|
|
237
|
+
logRequest('POST', '/edit-files', 500, `${c.red}${err.message}${c.reset}`);
|
|
238
|
+
json(res, 500, { error: err.message });
|
|
239
|
+
}
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
|
|
177
243
|
// POST /summary — decrypt summary, run AI prompt, encrypt new summary
|
|
178
244
|
if (req.method === 'POST' && url.pathname === '/summary') {
|
|
179
245
|
if (!authenticate(req, res)) {
|