@16pxh/cli-bridge 1.0.1 → 1.0.3

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/bin/bridge.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import * as readline from 'node:readline';
3
- import { setToken, clearToken } from '../lib/auth.mjs';
3
+ import { setToken, clearToken, loadPersistedToken } from '../lib/auth.mjs';
4
4
  import { checkClaudeInstalled, killActive } from '../lib/claude.mjs';
5
5
  import { startServer } from '../lib/server.mjs';
6
6
 
@@ -42,7 +42,13 @@ async function promptToken() {
42
42
  });
43
43
  }
44
44
 
45
- await promptToken();
45
+ // --- Load persisted token or prompt for new one ---
46
+ const loaded = loadPersistedToken();
47
+ if (loaded) {
48
+ console.log('✓ Previous token loaded from ~/.16pxh/bridge-token');
49
+ } else {
50
+ await promptToken();
51
+ }
46
52
 
47
53
  // --- Start server ---
48
54
  const server = startServer();
package/lib/auth.mjs CHANGED
@@ -1,6 +1,11 @@
1
1
  import { createHmac } from 'node:crypto';
2
+ import { readFileSync, writeFileSync, unlinkSync, mkdirSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { homedir } from 'node:os';
2
5
 
3
6
  const SECRET = '16pxh-bridge-v1';
7
+ const CONFIG_DIR = join(homedir(), '.16pxh');
8
+ const TOKEN_FILE = join(CONFIG_DIR, 'bridge-token');
4
9
 
5
10
  /** @type {string | null} */
6
11
  let storedHash = null;
@@ -15,18 +20,40 @@ function hashToken(rawToken) {
15
20
  }
16
21
 
17
22
  /**
18
- * Hash rawToken and store it in memory.
23
+ * Hash rawToken, store in memory + persist to ~/.16pxh/bridge-token.
19
24
  * @param {string} rawToken
20
25
  */
21
26
  export function setToken(rawToken) {
22
27
  storedHash = hashToken(rawToken);
28
+ try {
29
+ mkdirSync(CONFIG_DIR, { recursive: true });
30
+ writeFileSync(TOKEN_FILE, storedHash, 'utf-8');
31
+ } catch {
32
+ // Non-critical — memory still works
33
+ }
23
34
  }
24
35
 
25
36
  /**
26
- * Clear the stored token hash.
37
+ * Clear the stored token hash + delete persisted file.
27
38
  */
28
39
  export function clearToken() {
29
40
  storedHash = null;
41
+ try { unlinkSync(TOKEN_FILE); } catch { /* ignore */ }
42
+ }
43
+
44
+ /**
45
+ * Load persisted token hash from disk (if exists).
46
+ * @returns {boolean} true if a persisted token was loaded
47
+ */
48
+ export function loadPersistedToken() {
49
+ try {
50
+ const hash = readFileSync(TOKEN_FILE, 'utf-8').trim();
51
+ if (hash) {
52
+ storedHash = hash;
53
+ return true;
54
+ }
55
+ } catch { /* no persisted token */ }
56
+ return false;
30
57
  }
31
58
 
32
59
  /**
package/lib/server.mjs CHANGED
@@ -5,6 +5,40 @@ import { checkClaudeInstalled, runPrompt } from './claude.mjs';
5
5
  const PORT = 1676;
6
6
  const HOST = '127.0.0.1';
7
7
 
8
+ // ─── Colored logging ─────────────────────────────────────────────────────────
9
+ const c = {
10
+ reset: '\x1b[0m',
11
+ dim: '\x1b[2m',
12
+ green: '\x1b[32m',
13
+ yellow: '\x1b[33m',
14
+ red: '\x1b[31m',
15
+ cyan: '\x1b[36m',
16
+ magenta: '\x1b[35m',
17
+ };
18
+
19
+ function truncateWords(text, maxWords = 16) {
20
+ const words = text.replace(/\s+/g, ' ').trim().split(' ');
21
+ if (words.length <= maxWords) return words.join(' ');
22
+ return words.slice(0, maxWords).join(' ') + '…';
23
+ }
24
+
25
+ function truncateChars(text, maxChars = 64) {
26
+ const clean = text.replace(/\s+/g, ' ').trim();
27
+ if (clean.length <= maxChars) return clean;
28
+ return clean.slice(0, maxChars) + '…';
29
+ }
30
+
31
+ function timestamp() {
32
+ return new Date().toLocaleTimeString('en-US', { hour12: false });
33
+ }
34
+
35
+ function logRequest(method, path, status, extra = '') {
36
+ const statusColor = status < 400 ? c.green : status < 500 ? c.yellow : c.red;
37
+ console.log(
38
+ `${c.dim}${timestamp()}${c.reset} ${c.cyan}${method}${c.reset} ${path} ${statusColor}${status}${c.reset}${extra ? ' ' + extra : ''}`
39
+ );
40
+ }
41
+
8
42
  const CORS_HEADERS = {
9
43
  'Access-Control-Allow-Origin': '*',
10
44
  'Access-Control-Allow-Headers': 'x-bridge-token, content-type',
@@ -78,9 +112,12 @@ export function startServer() {
78
112
 
79
113
  // GET /health
80
114
  if (req.method === 'GET' && url.pathname === '/health') {
81
- if (!authenticate(req, res)) return;
82
-
115
+ if (!authenticate(req, res)) {
116
+ logRequest('GET', '/health', 403);
117
+ return;
118
+ }
83
119
  const version = await checkClaudeInstalled();
120
+ logRequest('GET', '/health', 200, `${c.dim}cli=${version ? 'ok' : 'missing'}${c.reset}`);
84
121
  json(res, 200, {
85
122
  status: 'ok',
86
123
  claude_cli: version !== null,
@@ -91,34 +128,46 @@ export function startServer() {
91
128
 
92
129
  // POST /prompt
93
130
  if (req.method === 'POST' && url.pathname === '/prompt') {
94
- if (!authenticate(req, res)) return;
131
+ if (!authenticate(req, res)) {
132
+ logRequest('POST', '/prompt', 403);
133
+ return;
134
+ }
95
135
 
96
136
  let body;
97
137
  try {
98
138
  const raw = await readBody(req);
99
139
  body = JSON.parse(raw);
100
140
  } catch {
141
+ logRequest('POST', '/prompt', 400, `${c.red}invalid JSON${c.reset}`);
101
142
  json(res, 400, { error: 'Invalid JSON body' });
102
143
  return;
103
144
  }
104
145
 
105
146
  const { prompt, images } = body;
106
147
  if (!prompt || typeof prompt !== 'string') {
148
+ logRequest('POST', '/prompt', 400, `${c.red}missing prompt${c.reset}`);
107
149
  json(res, 400, { error: 'Missing or invalid "prompt" field' });
108
150
  return;
109
151
  }
110
152
 
153
+ const promptPreview = truncateWords(prompt, 16);
154
+ console.log(`${c.dim}${timestamp()}${c.reset} ${c.magenta}→${c.reset} ${c.dim}${promptPreview}${c.reset}`);
155
+
111
156
  let fullPrompt = prompt;
112
157
  if (Array.isArray(images) && images.length > 0) {
113
- for (const url of images) {
114
- fullPrompt += `\n\nAnalyze this image: ${url}`;
158
+ for (const imgUrl of images) {
159
+ fullPrompt += `\n\nAnalyze this image: ${imgUrl}`;
115
160
  }
116
161
  }
117
162
 
118
163
  try {
119
164
  const { result, session_id, duration_ms } = await runPrompt(fullPrompt);
165
+ const resultPreview = truncateChars(result, 64);
166
+ logRequest('POST', '/prompt', 200, `${c.dim}${duration_ms}ms${c.reset}`);
167
+ console.log(`${c.dim}${timestamp()}${c.reset} ${c.green}←${c.reset} ${c.dim}${resultPreview}${c.reset}`);
120
168
  json(res, 200, { success: true, result, session_id, duration_ms });
121
169
  } catch (err) {
170
+ logRequest('POST', '/prompt', 500, `${c.red}${err.message}${c.reset}`);
122
171
  json(res, 500, { error: err.message });
123
172
  }
124
173
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@16pxh/cli-bridge",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "description": "Bridge local Claude CLI to 16pxh AI features",
5
5
  "type": "module",
6
6
  "bin": {