@rectify-so/bridge 0.1.1 → 0.1.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/dist/index.js +34 -2
- package/dist/readers/claude.js +126 -15
- package/dist/readers/codex.js +50 -3
- package/dist/server.js +15 -5
- package/dist/util.js +4 -2
- package/package.json +5 -1
package/dist/index.js
CHANGED
|
@@ -11,6 +11,7 @@ function parseArgs(argv) {
|
|
|
11
11
|
let command;
|
|
12
12
|
let token;
|
|
13
13
|
let apiBase = DEFAULT_API_BASE;
|
|
14
|
+
let local = false;
|
|
14
15
|
const positionals = [];
|
|
15
16
|
for (let i = 0; i < args.length; i++) {
|
|
16
17
|
const a = args[i];
|
|
@@ -20,6 +21,9 @@ function parseArgs(argv) {
|
|
|
20
21
|
else if (a.startsWith('--api=')) {
|
|
21
22
|
apiBase = a.slice('--api='.length);
|
|
22
23
|
}
|
|
24
|
+
else if (a === '--local') {
|
|
25
|
+
local = true;
|
|
26
|
+
}
|
|
23
27
|
else if (a === '--help' || a === '-h') {
|
|
24
28
|
command = 'help';
|
|
25
29
|
}
|
|
@@ -30,7 +34,7 @@ function parseArgs(argv) {
|
|
|
30
34
|
if (!command)
|
|
31
35
|
command = positionals[0];
|
|
32
36
|
token = positionals[1];
|
|
33
|
-
return { command, token, apiBase };
|
|
37
|
+
return { command, token, apiBase, local };
|
|
34
38
|
}
|
|
35
39
|
function printHelp() {
|
|
36
40
|
console.log(`
|
|
@@ -44,13 +48,15 @@ Commands:
|
|
|
44
48
|
|
|
45
49
|
Options:
|
|
46
50
|
--api <url> Override the Rectify API base URL (default: ${DEFAULT_API_BASE}).
|
|
51
|
+
--local Dev mode: bind the local server on the connection's port and
|
|
52
|
+
skip the SSH tunnel (for testing against a backend on this machine).
|
|
47
53
|
-h, --help Show this help.
|
|
48
54
|
|
|
49
55
|
Keep this window open while connected. Press Ctrl+C to disconnect.
|
|
50
56
|
`);
|
|
51
57
|
}
|
|
52
58
|
async function main() {
|
|
53
|
-
const { command, token, apiBase } = parseArgs(process.argv);
|
|
59
|
+
const { command, token, apiBase, local } = parseArgs(process.argv);
|
|
54
60
|
if (command === 'help' || !command) {
|
|
55
61
|
printHelp();
|
|
56
62
|
process.exit(command ? 0 : 1);
|
|
@@ -68,6 +74,32 @@ async function main() {
|
|
|
68
74
|
log('Fetching connection config from Rectify…');
|
|
69
75
|
const config = await api.fetchConfig();
|
|
70
76
|
log(`Connection type: ${config.mode}`);
|
|
77
|
+
// Local dev mode: bind the server on the connection's port and skip the tunnel.
|
|
78
|
+
// A backend on this same machine reaches the bridge directly at 127.0.0.1:<remotePort>.
|
|
79
|
+
if (local) {
|
|
80
|
+
if (config.mode === 'openclaw') {
|
|
81
|
+
warn('--local is only supported for Claude Code / Codex connections.');
|
|
82
|
+
process.exit(1);
|
|
83
|
+
}
|
|
84
|
+
const reader = config.mode === 'codex_local' ? codex : claude;
|
|
85
|
+
const health = await reader.checkHealth();
|
|
86
|
+
if (!health.ok)
|
|
87
|
+
warn(`Warning: ${health.error}`);
|
|
88
|
+
else
|
|
89
|
+
log(`${health.cli} detected${health.version ? ` (${health.version})` : ''}.`);
|
|
90
|
+
const server = await startLocalServer(config.mode, config.bridgeToken, config.remotePort);
|
|
91
|
+
void api.reportConnected({
|
|
92
|
+
os: process.platform,
|
|
93
|
+
cliInstalled: health.installed,
|
|
94
|
+
cliVersion: health.version ?? null,
|
|
95
|
+
});
|
|
96
|
+
log(`✓ Local dev mode — serving on 127.0.0.1:${server.port} (tunnel skipped).`);
|
|
97
|
+
log(` Your backend on this machine can reach the bridge directly. Keep this window open.`);
|
|
98
|
+
const shutdownLocal = () => { server.close(); process.exit(0); };
|
|
99
|
+
process.on('SIGINT', shutdownLocal);
|
|
100
|
+
process.on('SIGTERM', shutdownLocal);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
71
103
|
let localTarget;
|
|
72
104
|
let closeServer;
|
|
73
105
|
if (config.mode === 'openclaw') {
|
package/dist/readers/claude.js
CHANGED
|
@@ -50,10 +50,14 @@ function summarize(file, entries) {
|
|
|
50
50
|
let cwd = null;
|
|
51
51
|
let userMessages = 0;
|
|
52
52
|
let assistantMessages = 0;
|
|
53
|
+
let toolUses = 0;
|
|
53
54
|
let inputTokens = 0;
|
|
54
55
|
let outputTokens = 0;
|
|
56
|
+
let cacheReadTokens = 0;
|
|
57
|
+
let cacheCreationTokens = 0;
|
|
55
58
|
let firstTs = null;
|
|
56
59
|
let lastTs = null;
|
|
60
|
+
let lastUserPrompt = null;
|
|
57
61
|
for (const entry of entries) {
|
|
58
62
|
if (entry.gitBranch)
|
|
59
63
|
gitBranch = entry.gitBranch;
|
|
@@ -68,22 +72,41 @@ function summarize(file, entries) {
|
|
|
68
72
|
lastTs = ts;
|
|
69
73
|
}
|
|
70
74
|
}
|
|
75
|
+
// Skip subagent side-conversations so counts/tokens aren't inflated.
|
|
76
|
+
if (entry.isSidechain)
|
|
77
|
+
continue;
|
|
71
78
|
const msg = entry.message;
|
|
72
79
|
if (!msg)
|
|
73
80
|
continue;
|
|
74
81
|
if (msg.model)
|
|
75
82
|
model = msg.model;
|
|
76
|
-
if (msg.role === 'user')
|
|
83
|
+
if (msg.role === 'user') {
|
|
77
84
|
userMessages++;
|
|
78
|
-
|
|
85
|
+
if (typeof msg.content === 'string' && msg.content.length > 0) {
|
|
86
|
+
lastUserPrompt = msg.content.slice(0, 500);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if (msg.role === 'assistant') {
|
|
79
90
|
assistantMessages++;
|
|
91
|
+
if (Array.isArray(msg.content)) {
|
|
92
|
+
for (const block of msg.content) {
|
|
93
|
+
if (block.type === 'tool_use')
|
|
94
|
+
toolUses++;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
80
98
|
if (msg.usage) {
|
|
81
99
|
inputTokens += msg.usage.input_tokens ?? 0;
|
|
82
100
|
outputTokens += msg.usage.output_tokens ?? 0;
|
|
101
|
+
cacheReadTokens += msg.usage.cache_read_input_tokens ?? 0;
|
|
102
|
+
cacheCreationTokens += msg.usage.cache_creation_input_tokens ?? 0;
|
|
83
103
|
}
|
|
84
104
|
}
|
|
85
105
|
const pricing = (model && MODEL_PRICING[model]) || DEFAULT_PRICING;
|
|
86
|
-
const estimatedCost = inputTokens * pricing.input +
|
|
106
|
+
const estimatedCost = inputTokens * pricing.input +
|
|
107
|
+
cacheReadTokens * pricing.input * 0.1 +
|
|
108
|
+
cacheCreationTokens * pricing.input * 1.25 +
|
|
109
|
+
outputTokens * pricing.output;
|
|
87
110
|
const isActive = (lastTs ?? file.mtimeMs) > Date.now() - ACTIVE_THRESHOLD_MS;
|
|
88
111
|
const projectSlug = cwd ? basename(cwd) : 'claude';
|
|
89
112
|
return {
|
|
@@ -97,12 +120,16 @@ function summarize(file, entries) {
|
|
|
97
120
|
gitBranch,
|
|
98
121
|
userMessages,
|
|
99
122
|
assistantMessages,
|
|
123
|
+
toolUses,
|
|
100
124
|
inputTokens,
|
|
101
125
|
outputTokens,
|
|
102
|
-
|
|
103
|
-
|
|
126
|
+
cacheReadTokens,
|
|
127
|
+
cacheCreationTokens,
|
|
128
|
+
totalTokens: inputTokens + cacheReadTokens + cacheCreationTokens + outputTokens,
|
|
129
|
+
estimatedCost: Math.round(estimatedCost * 10000) / 10000,
|
|
104
130
|
firstMessageAt: firstTs ? new Date(firstTs).toISOString() : null,
|
|
105
131
|
lastMessageAt: lastTs ? new Date(lastTs).toISOString() : null,
|
|
132
|
+
lastUserPrompt,
|
|
106
133
|
status: isActive ? 'active' : 'idle',
|
|
107
134
|
isActive,
|
|
108
135
|
};
|
|
@@ -161,6 +188,33 @@ export async function listSessions(limit = 50, skip = 0) {
|
|
|
161
188
|
}
|
|
162
189
|
return { sessions, hasMore: slice.length > limit };
|
|
163
190
|
}
|
|
191
|
+
function toParts(content) {
|
|
192
|
+
if (typeof content === 'string') {
|
|
193
|
+
return content ? [{ type: 'text', text: content }] : [];
|
|
194
|
+
}
|
|
195
|
+
const parts = [];
|
|
196
|
+
for (const block of content) {
|
|
197
|
+
if (block.type === 'text' && block.text) {
|
|
198
|
+
parts.push({ type: 'text', text: block.text });
|
|
199
|
+
}
|
|
200
|
+
else if (block.type === 'thinking' && typeof block.thinking === 'string' && block.thinking.trim()) {
|
|
201
|
+
parts.push({ type: 'thinking', thinking: block.thinking.slice(0, 4000) });
|
|
202
|
+
}
|
|
203
|
+
else if (block.type === 'tool_use') {
|
|
204
|
+
const input = block.input !== undefined ? JSON.stringify(block.input) : '';
|
|
205
|
+
parts.push({ type: 'tool_use', name: block.name ?? 'tool', input: input.slice(0, 2000) });
|
|
206
|
+
}
|
|
207
|
+
else if (block.type === 'tool_result') {
|
|
208
|
+
const c = typeof block.content === 'string'
|
|
209
|
+
? block.content
|
|
210
|
+
: Array.isArray(block.content)
|
|
211
|
+
? block.content.map((b) => b?.text ?? '').join('\n')
|
|
212
|
+
: block.content !== undefined ? JSON.stringify(block.content) : '';
|
|
213
|
+
parts.push({ type: 'tool_result', content: String(c).slice(0, 4000), isError: block.is_error === true });
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return parts;
|
|
217
|
+
}
|
|
164
218
|
export async function sessionHistory(key, limit = 100, skip = 0) {
|
|
165
219
|
const files = await walkJsonl(projectsDir());
|
|
166
220
|
const target = files.find((f) => basename(f.path) === `${key}.jsonl`);
|
|
@@ -169,27 +223,63 @@ export async function sessionHistory(key, limit = 100, skip = 0) {
|
|
|
169
223
|
const content = await fs.readFile(target.path, 'utf8');
|
|
170
224
|
const entries = parseJsonl(content);
|
|
171
225
|
const messages = entries
|
|
172
|
-
.filter((e) => e.message?.role === 'user' || e.message?.role === 'assistant')
|
|
226
|
+
.filter((e) => !e.isSidechain && (e.message?.role === 'user' || e.message?.role === 'assistant'))
|
|
173
227
|
.map((e) => {
|
|
174
228
|
const m = e.message;
|
|
175
|
-
const
|
|
176
|
-
? m.content
|
|
177
|
-
: Array.isArray(m.content)
|
|
178
|
-
? m.content.filter((c) => c.type === 'text').map((c) => c.text ?? '').join('\n')
|
|
179
|
-
: '';
|
|
229
|
+
const parts = toParts(m.content ?? '');
|
|
180
230
|
return {
|
|
181
231
|
role: m.role ?? 'assistant',
|
|
182
|
-
content: text,
|
|
232
|
+
content: parts.length === 1 && parts[0].type === 'text' ? parts[0].text : parts,
|
|
183
233
|
timestamp: e.timestamp,
|
|
184
234
|
model: m.model ?? null,
|
|
185
235
|
usage: m.usage
|
|
186
236
|
? { input: m.usage.input_tokens ?? 0, output: m.usage.output_tokens ?? 0 }
|
|
187
237
|
: null,
|
|
188
238
|
};
|
|
189
|
-
})
|
|
239
|
+
})
|
|
240
|
+
.filter((m) => m.content.length > 0);
|
|
190
241
|
const sliced = messages.slice(skip, skip + limit);
|
|
191
242
|
return { messages: sliced, hasMore: skip + limit < messages.length };
|
|
192
243
|
}
|
|
244
|
+
export async function sessionUsage(key) {
|
|
245
|
+
const empty = { input: 0, output: 0, totalTokens: 0, cost: 0, totalCost: 0, requests: 0, requestCount: 0 };
|
|
246
|
+
const files = await walkJsonl(projectsDir());
|
|
247
|
+
const target = files.find((f) => basename(f.path) === `${key}.jsonl`);
|
|
248
|
+
if (!target)
|
|
249
|
+
return empty;
|
|
250
|
+
const content = await fs.readFile(target.path, 'utf8');
|
|
251
|
+
const summary = summarize(target, parseJsonl(content));
|
|
252
|
+
if (!summary)
|
|
253
|
+
return empty;
|
|
254
|
+
const input = summary.inputTokens + summary.cacheReadTokens + summary.cacheCreationTokens;
|
|
255
|
+
return {
|
|
256
|
+
input,
|
|
257
|
+
output: summary.outputTokens,
|
|
258
|
+
totalTokens: summary.totalTokens,
|
|
259
|
+
cost: summary.estimatedCost,
|
|
260
|
+
totalCost: summary.estimatedCost,
|
|
261
|
+
requests: summary.assistantMessages,
|
|
262
|
+
requestCount: summary.assistantMessages,
|
|
263
|
+
totals: { input, output: summary.outputTokens, total: summary.totalTokens, cost: summary.estimatedCost, requests: summary.assistantMessages },
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
async function resolveSessionCwd(key) {
|
|
267
|
+
const files = await walkJsonl(projectsDir());
|
|
268
|
+
const target = files.find((f) => basename(f.path) === `${key}.jsonl`);
|
|
269
|
+
if (!target)
|
|
270
|
+
return undefined;
|
|
271
|
+
try {
|
|
272
|
+
const content = await fs.readFile(target.path, 'utf8');
|
|
273
|
+
for (const entry of parseJsonl(content)) {
|
|
274
|
+
if (entry.cwd)
|
|
275
|
+
return entry.cwd;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
catch {
|
|
279
|
+
// ignore
|
|
280
|
+
}
|
|
281
|
+
return undefined;
|
|
282
|
+
}
|
|
193
283
|
export async function listSkills() {
|
|
194
284
|
const dir = join(claudeDir(), 'skills');
|
|
195
285
|
try {
|
|
@@ -218,8 +308,29 @@ export async function usage() {
|
|
|
218
308
|
}
|
|
219
309
|
return { totalCost, totalTokens, sessionCount: sessions.length };
|
|
220
310
|
}
|
|
221
|
-
export async function chat(message) {
|
|
222
|
-
|
|
311
|
+
export async function chat(message, sessionKey) {
|
|
312
|
+
// Only resume when the session key matches a real Claude session file
|
|
313
|
+
// (the Agents-page chat uses a synthetic key with no transcript to resume).
|
|
314
|
+
let resume = false;
|
|
315
|
+
let cwd;
|
|
316
|
+
if (sessionKey) {
|
|
317
|
+
const files = await walkJsonl(projectsDir());
|
|
318
|
+
const target = files.find((f) => basename(f.path) === `${sessionKey}.jsonl`);
|
|
319
|
+
if (target) {
|
|
320
|
+
resume = true;
|
|
321
|
+
cwd = await resolveSessionCwd(sessionKey);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
const tryRun = (doResume) => {
|
|
325
|
+
const args = ['--print'];
|
|
326
|
+
if (doResume && sessionKey)
|
|
327
|
+
args.push('--resume', sessionKey);
|
|
328
|
+
return run('claude', args, { input: message, timeoutMs: 180000, cwd });
|
|
329
|
+
};
|
|
330
|
+
let result = await tryRun(resume);
|
|
331
|
+
if (result.code !== 0 && resume && /no conversation found|not a uuid|valid session|not found|unknown session/i.test(result.stderr)) {
|
|
332
|
+
result = await tryRun(false);
|
|
333
|
+
}
|
|
223
334
|
if (result.code !== 0) {
|
|
224
335
|
throw new Error(result.stderr || result.stdout || `claude exited with code ${result.code}`);
|
|
225
336
|
}
|
package/dist/readers/codex.js
CHANGED
|
@@ -46,18 +46,26 @@ function summarize(file, entries) {
|
|
|
46
46
|
let model = null;
|
|
47
47
|
let userMessages = 0;
|
|
48
48
|
let assistantMessages = 0;
|
|
49
|
+
let toolUses = 0;
|
|
49
50
|
let inputTokens = 0;
|
|
50
51
|
let outputTokens = 0;
|
|
51
52
|
let firstTs = null;
|
|
52
53
|
let lastTs = null;
|
|
54
|
+
let lastUserPrompt = null;
|
|
53
55
|
for (const entry of entries) {
|
|
54
56
|
if (entry.model)
|
|
55
57
|
model = entry.model;
|
|
56
58
|
const role = entry.message?.role ?? entry.role;
|
|
57
|
-
if (role === 'user')
|
|
59
|
+
if (role === 'user') {
|
|
58
60
|
userMessages++;
|
|
61
|
+
const c = entry.message?.content ?? entry.content;
|
|
62
|
+
if (typeof c === 'string' && c.length > 0)
|
|
63
|
+
lastUserPrompt = c.slice(0, 500);
|
|
64
|
+
}
|
|
59
65
|
if (role === 'assistant')
|
|
60
66
|
assistantMessages++;
|
|
67
|
+
if (entry.type === 'function_call' || entry.type === 'tool_use' || entry.type === 'local_shell_call')
|
|
68
|
+
toolUses++;
|
|
61
69
|
if (entry.usage) {
|
|
62
70
|
inputTokens += entry.usage.input_tokens ?? entry.usage.prompt_tokens ?? 0;
|
|
63
71
|
outputTokens += entry.usage.output_tokens ?? entry.usage.completion_tokens ?? 0;
|
|
@@ -84,12 +92,16 @@ function summarize(file, entries) {
|
|
|
84
92
|
model,
|
|
85
93
|
userMessages,
|
|
86
94
|
assistantMessages,
|
|
95
|
+
toolUses,
|
|
87
96
|
inputTokens,
|
|
88
97
|
outputTokens,
|
|
98
|
+
cacheReadTokens: 0,
|
|
99
|
+
cacheCreationTokens: 0,
|
|
89
100
|
totalTokens: inputTokens + outputTokens,
|
|
90
101
|
estimatedCost: 0,
|
|
91
102
|
firstMessageAt: firstTs ? new Date(firstTs).toISOString() : null,
|
|
92
103
|
lastMessageAt: lastTs ? new Date(lastTs).toISOString() : null,
|
|
104
|
+
lastUserPrompt,
|
|
93
105
|
status: isActive ? 'active' : 'idle',
|
|
94
106
|
isActive,
|
|
95
107
|
};
|
|
@@ -184,6 +196,27 @@ export async function sessionHistory(key, limit = 100, skip = 0) {
|
|
|
184
196
|
const sliced = messages.slice(skip, skip + limit);
|
|
185
197
|
return { messages: sliced, hasMore: skip + limit < messages.length };
|
|
186
198
|
}
|
|
199
|
+
export async function sessionUsage(key) {
|
|
200
|
+
const empty = { input: 0, output: 0, totalTokens: 0, cost: 0, totalCost: 0, requests: 0, requestCount: 0 };
|
|
201
|
+
const files = await walkJsonl(sessionsDir());
|
|
202
|
+
const target = files.find((f) => deriveSessionId(f.path) === key);
|
|
203
|
+
if (!target)
|
|
204
|
+
return empty;
|
|
205
|
+
const content = await fs.readFile(target.path, 'utf8');
|
|
206
|
+
const summary = summarize(target, parseJsonl(content));
|
|
207
|
+
if (!summary)
|
|
208
|
+
return empty;
|
|
209
|
+
return {
|
|
210
|
+
input: summary.inputTokens,
|
|
211
|
+
output: summary.outputTokens,
|
|
212
|
+
totalTokens: summary.totalTokens,
|
|
213
|
+
cost: summary.estimatedCost,
|
|
214
|
+
totalCost: summary.estimatedCost,
|
|
215
|
+
requests: summary.assistantMessages,
|
|
216
|
+
requestCount: summary.assistantMessages,
|
|
217
|
+
totals: { input: summary.inputTokens, output: summary.outputTokens, total: summary.totalTokens, cost: summary.estimatedCost, requests: summary.assistantMessages },
|
|
218
|
+
};
|
|
219
|
+
}
|
|
187
220
|
export async function usage() {
|
|
188
221
|
const { sessions } = await listSessions(200, 0);
|
|
189
222
|
let totalTokens = 0;
|
|
@@ -191,8 +224,22 @@ export async function usage() {
|
|
|
191
224
|
totalTokens += s.totalTokens;
|
|
192
225
|
return { totalCost: 0, totalTokens, sessionCount: sessions.length };
|
|
193
226
|
}
|
|
194
|
-
export async function chat(message) {
|
|
195
|
-
|
|
227
|
+
export async function chat(message, sessionKey) {
|
|
228
|
+
// Only resume when the key matches a real session file (the Agents-page chat
|
|
229
|
+
// uses a synthetic key with no session to resume). cross-spawn passes the
|
|
230
|
+
// prompt as a discrete arg with no shell, so it can't be injected.
|
|
231
|
+
let resume = false;
|
|
232
|
+
if (sessionKey) {
|
|
233
|
+
const files = await walkJsonl(sessionsDir());
|
|
234
|
+
resume = files.some((f) => deriveSessionId(f.path) === sessionKey);
|
|
235
|
+
}
|
|
236
|
+
const args = resume && sessionKey
|
|
237
|
+
? ['exec', 'resume', sessionKey, message, '--skip-git-repo-check']
|
|
238
|
+
: ['exec', message];
|
|
239
|
+
let result = await run('codex', args, { timeoutMs: 180000 });
|
|
240
|
+
if (result.code !== 0 && resume && /not found|no session|unknown/i.test(result.stderr)) {
|
|
241
|
+
result = await run('codex', ['exec', message], { timeoutMs: 180000 });
|
|
242
|
+
}
|
|
196
243
|
if (result.code !== 0) {
|
|
197
244
|
throw new Error(result.stderr || result.stdout || `codex exited with code ${result.code}`);
|
|
198
245
|
}
|
package/dist/server.js
CHANGED
|
@@ -17,7 +17,7 @@ function sendJson(res, status, body) {
|
|
|
17
17
|
res.writeHead(status, { 'Content-Type': 'application/json' });
|
|
18
18
|
res.end(payload);
|
|
19
19
|
}
|
|
20
|
-
export function startLocalServer(mode, token) {
|
|
20
|
+
export function startLocalServer(mode, token, fixedPort = 0) {
|
|
21
21
|
const reader = readerFor(mode);
|
|
22
22
|
const server = http.createServer((req, res) => {
|
|
23
23
|
void handle(req, res).catch((err) => {
|
|
@@ -52,6 +52,12 @@ export function startLocalServer(mode, token) {
|
|
|
52
52
|
sendJson(res, 200, result);
|
|
53
53
|
return;
|
|
54
54
|
}
|
|
55
|
+
if (req.method === 'GET' && path.startsWith('/sessions/') && path.endsWith('/usage')) {
|
|
56
|
+
const key = decodeURIComponent(path.slice('/sessions/'.length, -'/usage'.length));
|
|
57
|
+
const result = await reader.sessionUsage(key);
|
|
58
|
+
sendJson(res, 200, result);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
55
61
|
if (req.method === 'GET' && path === '/skills') {
|
|
56
62
|
const result = 'listSkills' in reader ? await reader.listSkills() : { skills: [] };
|
|
57
63
|
sendJson(res, 200, result);
|
|
@@ -65,8 +71,11 @@ export function startLocalServer(mode, token) {
|
|
|
65
71
|
if (req.method === 'POST' && path === '/chat') {
|
|
66
72
|
const raw = await readBody(req);
|
|
67
73
|
let message = '';
|
|
74
|
+
let sessionKey;
|
|
68
75
|
try {
|
|
69
|
-
|
|
76
|
+
const body = JSON.parse(raw);
|
|
77
|
+
message = body.message ?? '';
|
|
78
|
+
sessionKey = body.sessionKey;
|
|
70
79
|
}
|
|
71
80
|
catch {
|
|
72
81
|
sendJson(res, 400, { error: 'Invalid JSON body' });
|
|
@@ -76,14 +85,15 @@ export function startLocalServer(mode, token) {
|
|
|
76
85
|
sendJson(res, 400, { error: 'message is required' });
|
|
77
86
|
return;
|
|
78
87
|
}
|
|
79
|
-
const content = await reader.chat(message);
|
|
88
|
+
const content = await reader.chat(message, sessionKey);
|
|
80
89
|
sendJson(res, 200, { content });
|
|
81
90
|
return;
|
|
82
91
|
}
|
|
83
92
|
sendJson(res, 404, { error: 'Not found' });
|
|
84
93
|
}
|
|
85
|
-
return new Promise((resolve) => {
|
|
86
|
-
server.
|
|
94
|
+
return new Promise((resolve, reject) => {
|
|
95
|
+
server.once('error', reject);
|
|
96
|
+
server.listen(fixedPort, '127.0.0.1', () => {
|
|
87
97
|
const addr = server.address();
|
|
88
98
|
const port = typeof addr === 'object' && addr ? addr.port : 0;
|
|
89
99
|
log(`Local API server listening on 127.0.0.1:${port}`);
|
package/dist/util.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import spawn from 'cross-spawn';
|
|
2
2
|
export function log(...args) {
|
|
3
3
|
const ts = new Date().toISOString().slice(11, 19);
|
|
4
4
|
console.log(`[${ts}]`, ...args);
|
|
@@ -11,7 +11,9 @@ const IS_WIN = process.platform === 'win32';
|
|
|
11
11
|
export function run(command, args, options = {}) {
|
|
12
12
|
const timeoutMs = options.timeoutMs ?? 120000;
|
|
13
13
|
return new Promise((resolve, reject) => {
|
|
14
|
-
|
|
14
|
+
// cross-spawn safely runs Windows .cmd/.bat shims WITHOUT a shell, so
|
|
15
|
+
// arguments (including user chat prompts) can never be interpreted as shell commands.
|
|
16
|
+
const child = spawn(command, args, { shell: false, cwd: options.cwd });
|
|
15
17
|
let stdout = '';
|
|
16
18
|
let stderr = '';
|
|
17
19
|
let settled = false;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rectify-so/bridge",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Rectify AgentPulse local bridge — securely connects locally-installed agent CLIs (Claude Code, Codex) and OpenClaw gateways to the Rectify dashboard over a reverse SSH tunnel.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -28,7 +28,11 @@
|
|
|
28
28
|
"tunnel"
|
|
29
29
|
],
|
|
30
30
|
"license": "MIT",
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"cross-spawn": "^7.0.6"
|
|
33
|
+
},
|
|
31
34
|
"devDependencies": {
|
|
35
|
+
"@types/cross-spawn": "^6.0.6",
|
|
32
36
|
"@types/node": "^20.11.0",
|
|
33
37
|
"typescript": "^5.4.0"
|
|
34
38
|
}
|