@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 +8 -2
- package/lib/auth.mjs +29 -2
- package/lib/server.mjs +54 -5
- package/package.json +1 -1
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
|
-
|
|
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
|
|
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))
|
|
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))
|
|
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
|
|
114
|
-
fullPrompt += `\n\nAnalyze this image: ${
|
|
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;
|