@kylindc/ccxray 1.2.0 → 1.2.2
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/.cursor/commands/opsx-apply.md +152 -0
- package/.cursor/commands/opsx-archive.md +157 -0
- package/.cursor/commands/opsx-continue.md +114 -0
- package/.cursor/commands/opsx-explore.md +173 -0
- package/.cursor/commands/opsx-propose.md +106 -0
- package/.cursor/commands/opsx-sync.md +134 -0
- package/.cursor/skills/openspec-apply-change/SKILL.md +156 -0
- package/.cursor/skills/openspec-archive-change/SKILL.md +114 -0
- package/.cursor/skills/openspec-continue-change/SKILL.md +118 -0
- package/.cursor/skills/openspec-explore/SKILL.md +288 -0
- package/.cursor/skills/openspec-propose/SKILL.md +110 -0
- package/.cursor/skills/openspec-sync-specs/SKILL.md +138 -0
- package/.idea/ccxray.iml +9 -0
- package/.idea/go.imports.xml +11 -0
- package/.idea/misc.xml +7 -0
- package/.idea/modules.xml +8 -0
- package/.idea/vcs.xml +6 -0
- package/package.json +1 -1
- package/server/config.js +2 -2
- package/server/forward.js +78 -4
- package/server/index.js +18 -3
package/server/forward.js
CHANGED
|
@@ -36,7 +36,16 @@ function forwardBedrockRequest(ctx) {
|
|
|
36
36
|
const { id, ts, startTime, parsedBody, rawBody, clientReq, clientRes, reqSessionId } = ctx;
|
|
37
37
|
|
|
38
38
|
const statsStripped = stripInjectedStats(parsedBody);
|
|
39
|
-
|
|
39
|
+
|
|
40
|
+
// Translate the Anthropic API body to Bedrock native format:
|
|
41
|
+
// - add anthropic_version (Bedrock requires it in the body; Claude Code sends it as a header)
|
|
42
|
+
// - strip `model` (encoded in the URL) and `stream` (indicated by the endpoint path)
|
|
43
|
+
const baseBody = parsedBody || JSON.parse(rawBody.toString());
|
|
44
|
+
const { model: _model, stream: _stream, ...bedrockFields } = baseBody;
|
|
45
|
+
const bodyToSend = Buffer.from(JSON.stringify({
|
|
46
|
+
anthropic_version: 'bedrock-2023-05-31',
|
|
47
|
+
...bedrockFields,
|
|
48
|
+
}));
|
|
40
49
|
|
|
41
50
|
// Resolve Bedrock model ID
|
|
42
51
|
let bedrockModelId;
|
|
@@ -49,6 +58,26 @@ function forwardBedrockRequest(ctx) {
|
|
|
49
58
|
}
|
|
50
59
|
clientRes.writeHead(400, { 'Content-Type': 'application/json' });
|
|
51
60
|
clientRes.end(JSON.stringify({ error: 'bedrock_model_unknown', message: err.message }));
|
|
61
|
+
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
62
|
+
const errEvent = [{ type: 'error', error: 'bedrock_model_unknown', message: err.message }];
|
|
63
|
+
const resWritePromise = config.storage.write(id, '_res.json', JSON.stringify(errEvent))
|
|
64
|
+
.catch(e => console.error('Write res.json failed:', e.message));
|
|
65
|
+
const entry = {
|
|
66
|
+
id, ts, sessionId: reqSessionId, method: clientReq.method, url: clientReq.url,
|
|
67
|
+
req: parsedBody, res: errEvent, elapsed, status: 400, isSSE: false,
|
|
68
|
+
tokens: helpers.tokenizeRequest(parsedBody),
|
|
69
|
+
usage: null, cost: null, maxContext: null,
|
|
70
|
+
cwd: store.sessionMeta[reqSessionId]?.cwd || null,
|
|
71
|
+
receivedAt: startTime, model: parsedBody?.model || null,
|
|
72
|
+
msgCount: parsedBody?.messages?.length || 0, toolCount: parsedBody?.tools?.length || 0,
|
|
73
|
+
stopReason: 'error', title: null,
|
|
74
|
+
sysHash: ctx.sysHash || null, toolsHash: ctx.toolsHash || null,
|
|
75
|
+
};
|
|
76
|
+
entry._writePromise = Promise.all([ctx.reqWritePromise, resWritePromise].filter(Boolean));
|
|
77
|
+
store.entries.push(entry);
|
|
78
|
+
store.trimEntries();
|
|
79
|
+
broadcast(entry);
|
|
80
|
+
console.error(`\x1b[31m❌ BEDROCK MODEL UNKNOWN: ${err.message}\x1b[0m`);
|
|
52
81
|
return;
|
|
53
82
|
}
|
|
54
83
|
|
|
@@ -67,8 +96,8 @@ function forwardBedrockRequest(ctx) {
|
|
|
67
96
|
fwdHeaders['host'] = url.hostname;
|
|
68
97
|
|
|
69
98
|
// Auth: bearer token takes precedence over SigV4
|
|
70
|
-
if (config.
|
|
71
|
-
fwdHeaders['authorization'] = `Bearer ${config.
|
|
99
|
+
if (config.AWS_BEARER_TOKEN_BEDROCK) {
|
|
100
|
+
fwdHeaders['authorization'] = `Bearer ${config.AWS_BEARER_TOKEN_BEDROCK}`;
|
|
72
101
|
} else {
|
|
73
102
|
const creds = config.BEDROCK_CREDENTIALS;
|
|
74
103
|
const signed = sigv4.sign('POST', bedrockUrl, fwdHeaders, bodyToSend, creds, config.BEDROCK_RESOLVED_REGION, 'bedrock');
|
|
@@ -109,6 +138,26 @@ function forwardBedrockRequest(ctx) {
|
|
|
109
138
|
clientRes.writeHead(502, { 'Content-Type': 'application/json' });
|
|
110
139
|
}
|
|
111
140
|
clientRes.end(JSON.stringify({ error: 'proxy_error', message: err.message }));
|
|
141
|
+
|
|
142
|
+
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
143
|
+
const errEvent = [{ type: 'error', error: 'proxy_error', message: err.message }];
|
|
144
|
+
const resWritePromise = config.storage.write(id, '_res.json', JSON.stringify(errEvent))
|
|
145
|
+
.catch(e => console.error('Write res.json failed:', e.message));
|
|
146
|
+
const entry = {
|
|
147
|
+
id, ts, sessionId: reqSessionId, method: clientReq.method, url: clientReq.url,
|
|
148
|
+
req: parsedBody, res: errEvent, elapsed, status: 502, isSSE: false,
|
|
149
|
+
tokens: helpers.tokenizeRequest(parsedBody),
|
|
150
|
+
usage: null, cost: null, maxContext: null,
|
|
151
|
+
cwd: store.sessionMeta[reqSessionId]?.cwd || null,
|
|
152
|
+
receivedAt: startTime, model: parsedBody?.model || null,
|
|
153
|
+
msgCount: parsedBody?.messages?.length || 0, toolCount: parsedBody?.tools?.length || 0,
|
|
154
|
+
stopReason: 'error', title: null,
|
|
155
|
+
sysHash: ctx.sysHash || null, toolsHash: ctx.toolsHash || null,
|
|
156
|
+
};
|
|
157
|
+
entry._writePromise = Promise.all([ctx.reqWritePromise, resWritePromise].filter(Boolean));
|
|
158
|
+
store.entries.push(entry);
|
|
159
|
+
store.trimEntries();
|
|
160
|
+
broadcast(entry);
|
|
112
161
|
});
|
|
113
162
|
|
|
114
163
|
proxyReq.end(bodyToSend);
|
|
@@ -166,7 +215,32 @@ function handleBedrockSSEResponse(ctx, proxyRes, clientRes, bedrockModelId) {
|
|
|
166
215
|
});
|
|
167
216
|
|
|
168
217
|
proxyRes.on('end', () => {
|
|
169
|
-
if (streamErrored)
|
|
218
|
+
if (streamErrored) {
|
|
219
|
+
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
220
|
+
const errEvent = [{ type: 'error', error: 'modelStreamErrorException' }];
|
|
221
|
+
const resWritePromise = config.storage.write(id, '_res.json', JSON.stringify(errEvent))
|
|
222
|
+
.catch(e => console.error('Write res.json failed:', e.message));
|
|
223
|
+
if (reqSessionId) {
|
|
224
|
+
store.activeRequests[reqSessionId] = Math.max(0, (store.activeRequests[reqSessionId] || 1) - 1);
|
|
225
|
+
broadcastSessionStatus(reqSessionId);
|
|
226
|
+
}
|
|
227
|
+
const entry = {
|
|
228
|
+
id, ts: ctx.ts, sessionId: reqSessionId, method: ctx.clientReq.method, url: ctx.clientReq.url,
|
|
229
|
+
req: parsedBody, res: errEvent, elapsed, status: 500, isSSE: true,
|
|
230
|
+
tokens: helpers.tokenizeRequest(parsedBody),
|
|
231
|
+
usage: null, cost: null, maxContext: null,
|
|
232
|
+
cwd: store.sessionMeta[reqSessionId]?.cwd || null,
|
|
233
|
+
receivedAt: startTime, model: parsedBody?.model || null,
|
|
234
|
+
msgCount: parsedBody?.messages?.length || 0, toolCount: parsedBody?.tools?.length || 0,
|
|
235
|
+
stopReason: 'error', title: null,
|
|
236
|
+
sysHash: ctx.sysHash || null, toolsHash: ctx.toolsHash || null,
|
|
237
|
+
};
|
|
238
|
+
entry._writePromise = Promise.all([ctx.reqWritePromise, resWritePromise].filter(Boolean));
|
|
239
|
+
store.entries.push(entry);
|
|
240
|
+
store.trimEntries();
|
|
241
|
+
broadcast(entry);
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
170
244
|
|
|
171
245
|
if (ctx.skipEntry) {
|
|
172
246
|
for (const held of heldEventStrs) clientRes.write(held);
|
package/server/index.js
CHANGED
|
@@ -291,6 +291,12 @@ function spawnClaude(port, args) {
|
|
|
291
291
|
delete childEnv[key];
|
|
292
292
|
}
|
|
293
293
|
}
|
|
294
|
+
// Bedrock users typically have no ANTHROPIC_API_KEY. Without it, Claude Code
|
|
295
|
+
// refuses to start ("Not logged in"). Set a placeholder — ccxray strips the
|
|
296
|
+
// x-api-key header before forwarding to Bedrock, so it never reaches AWS.
|
|
297
|
+
if (!childEnv.ANTHROPIC_API_KEY) {
|
|
298
|
+
childEnv.ANTHROPIC_API_KEY = 'bedrock-via-ccxray';
|
|
299
|
+
}
|
|
294
300
|
}
|
|
295
301
|
const child = spawn('claude', args, {
|
|
296
302
|
stdio: 'inherit',
|
|
@@ -405,9 +411,18 @@ async function startClientMode(lock) {
|
|
|
405
411
|
|
|
406
412
|
// Spawn claude pointing to hub
|
|
407
413
|
const { spawn } = require('child_process');
|
|
414
|
+
let hubClientEnv = { ...process.env, ANTHROPIC_BASE_URL: `http://localhost:${lock.port}` };
|
|
415
|
+
if (config.IS_BEDROCK_MODE) {
|
|
416
|
+
for (const key of Object.keys(hubClientEnv)) {
|
|
417
|
+
if (key.startsWith('AWS_') || key === 'CLAUDE_CODE_USE_BEDROCK') delete hubClientEnv[key];
|
|
418
|
+
}
|
|
419
|
+
if (!hubClientEnv.ANTHROPIC_API_KEY) {
|
|
420
|
+
hubClientEnv.ANTHROPIC_API_KEY = 'bedrock-via-ccxray';
|
|
421
|
+
}
|
|
422
|
+
}
|
|
408
423
|
const child = spawn('claude', claudeArgs, {
|
|
409
424
|
stdio: 'inherit',
|
|
410
|
-
env:
|
|
425
|
+
env: hubClientEnv,
|
|
411
426
|
});
|
|
412
427
|
child.on('error', (err) => {
|
|
413
428
|
if (err.code === 'ENOENT') {
|
|
@@ -435,13 +450,13 @@ async function startServer() {
|
|
|
435
450
|
if (config.IS_BEDROCK_MODE) {
|
|
436
451
|
const activationSource = config.BEDROCK_ACTIVATION_SOURCE
|
|
437
452
|
|| (process.argv.includes('--bedrock') ? '--bedrock flag' : 'unknown');
|
|
438
|
-
if (config.
|
|
453
|
+
if (config.AWS_BEARER_TOKEN_BEDROCK) {
|
|
439
454
|
_origLog(`\x1b[36mBedrock mode: ${config.BEDROCK_RESOLVED_REGION} (bearer token auth, via ${activationSource})\x1b[0m`);
|
|
440
455
|
} else {
|
|
441
456
|
const { resolveCredentials } = require('./bedrock-credentials');
|
|
442
457
|
const creds = await resolveCredentials();
|
|
443
458
|
if (!creds) {
|
|
444
|
-
console.error('\x1b[31mBedrock mode requires auth. Set
|
|
459
|
+
console.error('\x1b[31mBedrock mode requires auth. Set AWS_BEARER_TOKEN_BEDROCK, AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY, or configure ~/.aws/credentials\x1b[0m');
|
|
445
460
|
process.exit(1);
|
|
446
461
|
}
|
|
447
462
|
config.BEDROCK_CREDENTIALS = creds;
|