@16pxh/cli-bridge 1.0.3 → 1.0.6

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
@@ -22,12 +22,13 @@ export async function checkClaudeInstalled() {
22
22
  /**
23
23
  * Spawn `claude -p <prompt> --output-format json`, resolve with parsed result.
24
24
  * @param {string} prompt
25
- * @param {{ timeoutMs?: number }} [options]
25
+ * @param {{ sessionId?: string }} [options]
26
26
  * @returns {Promise<{ result: string, session_id: string, duration_ms: number }>}
27
27
  */
28
- export function runPrompt(prompt, { timeoutMs = 60000 } = {}) {
28
+ export function runPrompt(prompt, { sessionId } = {}) {
29
29
  return new Promise((resolve, reject) => {
30
30
  const args = ['-p', prompt, '--output-format', 'json'];
31
+ if (sessionId) args.push('--resume', sessionId);
31
32
  const child = spawn('claude', args, { stdio: ['ignore', 'pipe', 'pipe'] });
32
33
 
33
34
  activeProcess = child;
@@ -38,13 +39,7 @@ export function runPrompt(prompt, { timeoutMs = 60000 } = {}) {
38
39
  child.stdout.on('data', (chunk) => { stdout += chunk; });
39
40
  child.stderr.on('data', (chunk) => { stderr += chunk; });
40
41
 
41
- const timer = setTimeout(() => {
42
- child.kill('SIGKILL');
43
- reject(new Error(`Claude CLI timed out after ${timeoutMs}ms`));
44
- }, timeoutMs);
45
-
46
42
  child.on('close', (code) => {
47
- clearTimeout(timer);
48
43
  if (activeProcess === child) activeProcess = null;
49
44
 
50
45
  if (code !== 0) {
package/lib/crypto.mjs ADDED
@@ -0,0 +1,44 @@
1
+ import { createHash, createCipheriv, createDecipheriv, randomBytes } from 'node:crypto';
2
+
3
+ const ALGORITHM = 'aes-256-cbc';
4
+ const CACHE_TTL = 5 * 60 * 1000;
5
+
6
+ let cachedSecret = null;
7
+ let cachedAt = 0;
8
+
9
+ export async function getBridgeSecret() {
10
+ if (cachedSecret && Date.now() - cachedAt < CACHE_TTL) return cachedSecret;
11
+ const url = process.env.BRIDGE_SECRET_URL;
12
+ if (!url) return null;
13
+ try {
14
+ const res = await fetch(url);
15
+ if (!res.ok) return null;
16
+ cachedSecret = (await res.text()).trim();
17
+ cachedAt = Date.now();
18
+ return cachedSecret;
19
+ } catch {
20
+ return null;
21
+ }
22
+ }
23
+
24
+ function deriveKey(secret) {
25
+ return createHash('sha256').update(secret).digest();
26
+ }
27
+
28
+ export async function encryptSummary(text) {
29
+ const secret = await getBridgeSecret();
30
+ if (!secret) throw new Error('Bridge secret not available');
31
+ const key = deriveKey(secret);
32
+ const iv = randomBytes(16);
33
+ const cipher = createCipheriv(ALGORITHM, key, iv);
34
+ return iv.toString('hex') + ':' + cipher.update(text, 'utf8', 'hex') + cipher.final('hex');
35
+ }
36
+
37
+ export async function decryptSummary(encrypted) {
38
+ const secret = await getBridgeSecret();
39
+ if (!secret) throw new Error('Bridge secret not available');
40
+ const key = deriveKey(secret);
41
+ const [ivHex, data] = encrypted.split(':');
42
+ const decipher = createDecipheriv(ALGORITHM, key, Buffer.from(ivHex, 'hex'));
43
+ return decipher.update(data, 'hex', 'utf8') + decipher.final('utf8');
44
+ }
package/lib/server.mjs CHANGED
@@ -1,6 +1,7 @@
1
1
  import { createServer } from 'node:http';
2
2
  import { verifyToken, hasToken } from './auth.mjs';
3
3
  import { checkClaudeInstalled, runPrompt } from './claude.mjs';
4
+ import { decryptSummary, encryptSummary } from './crypto.mjs';
4
5
 
5
6
  const PORT = 1676;
6
7
  const HOST = '127.0.0.1';
@@ -143,7 +144,7 @@ export function startServer() {
143
144
  return;
144
145
  }
145
146
 
146
- const { prompt, images } = body;
147
+ const { prompt, images, session_id } = body;
147
148
  if (!prompt || typeof prompt !== 'string') {
148
149
  logRequest('POST', '/prompt', 400, `${c.red}missing prompt${c.reset}`);
149
150
  json(res, 400, { error: 'Missing or invalid "prompt" field' });
@@ -161,11 +162,11 @@ export function startServer() {
161
162
  }
162
163
 
163
164
  try {
164
- const { result, session_id, duration_ms } = await runPrompt(fullPrompt);
165
+ const { result, session_id: responseSessionId, duration_ms } = await runPrompt(fullPrompt, { sessionId: session_id });
165
166
  const resultPreview = truncateChars(result, 64);
166
167
  logRequest('POST', '/prompt', 200, `${c.dim}${duration_ms}ms${c.reset}`);
167
168
  console.log(`${c.dim}${timestamp()}${c.reset} ${c.green}←${c.reset} ${c.dim}${resultPreview}${c.reset}`);
168
- json(res, 200, { success: true, result, session_id, duration_ms });
169
+ json(res, 200, { success: true, result, session_id: responseSessionId, duration_ms });
169
170
  } catch (err) {
170
171
  logRequest('POST', '/prompt', 500, `${c.red}${err.message}${c.reset}`);
171
172
  json(res, 500, { error: err.message });
@@ -173,6 +174,65 @@ export function startServer() {
173
174
  return;
174
175
  }
175
176
 
177
+ // POST /summary — decrypt summary, run AI prompt, encrypt new summary
178
+ if (req.method === 'POST' && url.pathname === '/summary') {
179
+ if (!authenticate(req, res)) {
180
+ logRequest('POST', '/summary', 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', '/summary', 400, `${c.red}invalid JSON${c.reset}`);
190
+ json(res, 400, { error: 'Invalid JSON body' });
191
+ return;
192
+ }
193
+
194
+ const { prompt, encrypted_summary } = body;
195
+ if (!prompt) {
196
+ json(res, 400, { error: 'Missing prompt' });
197
+ return;
198
+ }
199
+
200
+ try {
201
+ let currentSummary = '';
202
+ if (encrypted_summary) {
203
+ currentSummary = await decryptSummary(encrypted_summary);
204
+ }
205
+
206
+ const fullPrompt = prompt.replace('{current_summary}', currentSummary || '(empty — this is the first activity)');
207
+
208
+ const promptPreview = truncateWords(fullPrompt, 12);
209
+ console.log(`${c.dim}${timestamp()}${c.reset} ${c.magenta}→ summary${c.reset} ${c.dim}${promptPreview}${c.reset}`);
210
+
211
+ const { result, duration_ms } = await runPrompt(fullPrompt);
212
+
213
+ const jsonMatch = result.match(/\{[\s\S]*\}/);
214
+ if (!jsonMatch) throw new Error('No JSON in response');
215
+ const parsed = JSON.parse(jsonMatch[0]);
216
+
217
+ const encryptedNewSummary = await encryptSummary(parsed.summary || '');
218
+
219
+ const resultPreview = truncateChars(parsed.writing_style || '', 64);
220
+ logRequest('POST', '/summary', 200, `${c.dim}${duration_ms}ms${c.reset}`);
221
+ console.log(`${c.dim}${timestamp()}${c.reset} ${c.green}← style:${c.reset} ${c.dim}${resultPreview}${c.reset}`);
222
+
223
+ json(res, 200, {
224
+ success: true,
225
+ encrypted_summary: encryptedNewSummary,
226
+ writing_style: parsed.writing_style || '',
227
+ duration_ms,
228
+ });
229
+ } catch (err) {
230
+ logRequest('POST', '/summary', 500, `${c.red}${err.message}${c.reset}`);
231
+ json(res, 500, { error: err.message });
232
+ }
233
+ return;
234
+ }
235
+
176
236
  // 404 fallback
177
237
  json(res, 404, { error: 'Not found' });
178
238
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@16pxh/cli-bridge",
3
- "version": "1.0.3",
3
+ "version": "1.0.6",
4
4
  "description": "Bridge local Claude CLI to 16pxh AI features",
5
5
  "type": "module",
6
6
  "bin": {