opencode-cluade-auth 1.0.0
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/README.md +104 -0
- package/dist/anthropic-compat.d.ts +71 -0
- package/dist/anthropic-compat.js +342 -0
- package/dist/claude-cli.d.ts +21 -0
- package/dist/claude-cli.js +274 -0
- package/dist/fetch.d.ts +5 -0
- package/dist/fetch.js +70 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.js +36 -0
- package/dist/smoke.d.ts +1 -0
- package/dist/smoke.js +27 -0
- package/package.json +47 -0
package/README.md
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# opencode-cluade-auth
|
|
2
|
+
|
|
3
|
+
Claude Code CLI-backed Anthropic auth plugin for OpenCode.
|
|
4
|
+
|
|
5
|
+
This package registers an `anthropic` auth hook and translates Anthropic-style
|
|
6
|
+
`/v1/messages` requests into local `claude` CLI executions instead of reusing
|
|
7
|
+
Claude OAuth or API-key transport directly.
|
|
8
|
+
|
|
9
|
+
## Quick Start
|
|
10
|
+
|
|
11
|
+
1. Install the package where OpenCode can resolve it.
|
|
12
|
+
2. Make sure the local `claude` CLI is installed and already logged in.
|
|
13
|
+
3. Add the plugin to your OpenCode config.
|
|
14
|
+
|
|
15
|
+
```json
|
|
16
|
+
{
|
|
17
|
+
"$schema": "https://opencode.ai/config.json",
|
|
18
|
+
"plugin": ["opencode-cluade-auth@latest"],
|
|
19
|
+
"provider": {
|
|
20
|
+
"anthropic": {
|
|
21
|
+
"name": "Anthropic",
|
|
22
|
+
"npm": "@ai-sdk/anthropic"
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
For local development, you can also point OpenCode at the built entry directly:
|
|
29
|
+
|
|
30
|
+
```json
|
|
31
|
+
{
|
|
32
|
+
"plugin": ["/absolute/path/to/opencode-cluade-auth/dist/index.js"]
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## What It Does
|
|
37
|
+
|
|
38
|
+
- seeds a local `anthropic` auth entry for OpenCode
|
|
39
|
+
- proxies Anthropic-style `/v1/messages` requests to the local `claude` CLI
|
|
40
|
+
- converts Claude CLI output back into Anthropic-style JSON or buffered SSE
|
|
41
|
+
- supports normal text replies and tool-calling-compatible response envelopes
|
|
42
|
+
|
|
43
|
+
## Requirements
|
|
44
|
+
|
|
45
|
+
- Claude Code CLI installed and logged in on the same machine
|
|
46
|
+
- `claude --help` must expose the safe flags used by the adapter:
|
|
47
|
+
- `-p` or `--print`
|
|
48
|
+
- `--output-format`
|
|
49
|
+
- `--permission-mode plan`
|
|
50
|
+
- `--tools`
|
|
51
|
+
- `--no-session-persistence`
|
|
52
|
+
|
|
53
|
+
## Runtime Behavior
|
|
54
|
+
|
|
55
|
+
- the main user prompt is sent over `stdin`
|
|
56
|
+
- large system prompts are written to a temporary file and passed via `--system-prompt-file`
|
|
57
|
+
- the adapter runs Claude in non-interactive print mode with conservative flags
|
|
58
|
+
- the plugin returns structured fetch errors when the local CLI is missing or exits unsuccessfully
|
|
59
|
+
|
|
60
|
+
## Configuration
|
|
61
|
+
|
|
62
|
+
Optional binary overrides:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
export OPENCODE_CLAUDE_AUTH_BIN="/path/to/claude"
|
|
66
|
+
# legacy/internal compatibility alias
|
|
67
|
+
export AEGIS_CLAUDE_CODE_CLI_BIN="/path/to/claude"
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Programmatic usage:
|
|
71
|
+
|
|
72
|
+
```ts
|
|
73
|
+
import ClaudeCodeCLIAuthPlugin from "opencode-cluade-auth";
|
|
74
|
+
|
|
75
|
+
const plugin = await ClaudeCodeCLIAuthPlugin();
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Verification
|
|
79
|
+
|
|
80
|
+
These checks are used during development before publish:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
npm run build
|
|
84
|
+
bun run typecheck
|
|
85
|
+
bun test
|
|
86
|
+
bun run smoke -- "Reply with exactly SMOKE_OK"
|
|
87
|
+
bun run smoke -- --stream "Reply with exactly SMOKE_OK"
|
|
88
|
+
npm pack --dry-run
|
|
89
|
+
npm publish --dry-run
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Limitations
|
|
93
|
+
|
|
94
|
+
- streaming responses are exposed as buffered Anthropic SSE, not live token passthrough
|
|
95
|
+
- the package depends on the local `claude` CLI login state
|
|
96
|
+
- this package does not bundle the Claude CLI itself
|
|
97
|
+
|
|
98
|
+
## Development
|
|
99
|
+
|
|
100
|
+
- `npm run build`
|
|
101
|
+
- `bun run typecheck`
|
|
102
|
+
- `bun test`
|
|
103
|
+
- `bun run smoke -- "Reply with exactly SMOKE_OK"`
|
|
104
|
+
- `bun run smoke -- --stream "Reply with exactly SMOKE_OK"`
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
type AnthropicMessageRole = "user" | "assistant";
|
|
2
|
+
export type AnthropicMessage = {
|
|
3
|
+
role: AnthropicMessageRole;
|
|
4
|
+
content: string;
|
|
5
|
+
};
|
|
6
|
+
export type AnthropicTool = {
|
|
7
|
+
name: string;
|
|
8
|
+
description?: string;
|
|
9
|
+
inputSchema?: Record<string, unknown>;
|
|
10
|
+
};
|
|
11
|
+
export type ClaudePreparedRequest = {
|
|
12
|
+
systemPrompt?: string;
|
|
13
|
+
userPrompt: string;
|
|
14
|
+
outputFormat: "text" | "json";
|
|
15
|
+
};
|
|
16
|
+
type ClaudeResponseEnvelope = {
|
|
17
|
+
type: "final";
|
|
18
|
+
content: string;
|
|
19
|
+
} | {
|
|
20
|
+
type: "tool-calls";
|
|
21
|
+
tool_calls: Array<{
|
|
22
|
+
id: string;
|
|
23
|
+
name: string;
|
|
24
|
+
arguments: Record<string, unknown>;
|
|
25
|
+
}>;
|
|
26
|
+
};
|
|
27
|
+
export declare function extractAnthropicRequest(raw: unknown): {
|
|
28
|
+
ok: true;
|
|
29
|
+
model: string;
|
|
30
|
+
system: string;
|
|
31
|
+
messages: AnthropicMessage[];
|
|
32
|
+
tools: AnthropicTool[];
|
|
33
|
+
stream: boolean;
|
|
34
|
+
} | {
|
|
35
|
+
ok: false;
|
|
36
|
+
status: number;
|
|
37
|
+
reason: string;
|
|
38
|
+
};
|
|
39
|
+
export declare function buildClaudeUserPrompt(messages: AnthropicMessage[]): string;
|
|
40
|
+
export declare function buildClaudePreparedRequest(params: {
|
|
41
|
+
system: string;
|
|
42
|
+
messages: AnthropicMessage[];
|
|
43
|
+
tools: AnthropicTool[];
|
|
44
|
+
}): ClaudePreparedRequest;
|
|
45
|
+
export declare function parseClaudeResponseEnvelope(text: string): ClaudeResponseEnvelope | null;
|
|
46
|
+
export declare function buildAnthropicMessageResponse(params: {
|
|
47
|
+
model: string;
|
|
48
|
+
content: string;
|
|
49
|
+
}): Response;
|
|
50
|
+
export declare function buildAnthropicToolUseResponse(params: {
|
|
51
|
+
model: string;
|
|
52
|
+
toolCalls: Array<{
|
|
53
|
+
id: string;
|
|
54
|
+
name: string;
|
|
55
|
+
arguments: Record<string, unknown>;
|
|
56
|
+
}>;
|
|
57
|
+
}): Response;
|
|
58
|
+
export declare function buildAnthropicErrorResponse(status: number, message: string): Response;
|
|
59
|
+
export declare function buildAnthropicMessageStreamResponse(params: {
|
|
60
|
+
model: string;
|
|
61
|
+
content: string;
|
|
62
|
+
}): Response;
|
|
63
|
+
export declare function buildAnthropicToolUseStreamResponse(params: {
|
|
64
|
+
model: string;
|
|
65
|
+
toolCalls: Array<{
|
|
66
|
+
id: string;
|
|
67
|
+
name: string;
|
|
68
|
+
arguments: Record<string, unknown>;
|
|
69
|
+
}>;
|
|
70
|
+
}): Response;
|
|
71
|
+
export {};
|
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
function isRecord(value) {
|
|
2
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3
|
+
}
|
|
4
|
+
function asText(value) {
|
|
5
|
+
return typeof value === "string" ? value : "";
|
|
6
|
+
}
|
|
7
|
+
function pushSection(lines, title, content) {
|
|
8
|
+
const trimmed = content.trim();
|
|
9
|
+
if (!trimmed) {
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
lines.push(`${title}:`, trimmed, "");
|
|
13
|
+
}
|
|
14
|
+
function extractContentText(content) {
|
|
15
|
+
if (typeof content === "string") {
|
|
16
|
+
return content;
|
|
17
|
+
}
|
|
18
|
+
if (!Array.isArray(content)) {
|
|
19
|
+
return "";
|
|
20
|
+
}
|
|
21
|
+
const parts = [];
|
|
22
|
+
for (const block of content) {
|
|
23
|
+
if (!isRecord(block)) {
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
if (block.type === "text") {
|
|
27
|
+
const text = asText(block.text).trim();
|
|
28
|
+
if (text) {
|
|
29
|
+
parts.push(text);
|
|
30
|
+
}
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
if (block.type === "tool_result") {
|
|
34
|
+
const toolUseId = asText(block.tool_use_id).trim();
|
|
35
|
+
const toolText = extractContentText(block.content).trim();
|
|
36
|
+
const prefix = toolUseId ? `[tool_result ${toolUseId}]` : "[tool_result]";
|
|
37
|
+
parts.push(toolText ? `${prefix} ${toolText}` : prefix);
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
if (block.type === "tool_use") {
|
|
41
|
+
const name = asText(block.name).trim();
|
|
42
|
+
const id = asText(block.id).trim();
|
|
43
|
+
const input = isRecord(block.input) ? JSON.stringify(block.input) : "{}";
|
|
44
|
+
const label = ["[tool_use", name, id].filter(Boolean).join(" ");
|
|
45
|
+
parts.push(`${label}] ${input}`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return parts.join("\n");
|
|
49
|
+
}
|
|
50
|
+
export function extractAnthropicRequest(raw) {
|
|
51
|
+
if (!isRecord(raw)) {
|
|
52
|
+
return { ok: false, status: 400, reason: "invalid request body" };
|
|
53
|
+
}
|
|
54
|
+
const model = asText(raw.model).trim();
|
|
55
|
+
if (!model) {
|
|
56
|
+
return { ok: false, status: 400, reason: "model is required" };
|
|
57
|
+
}
|
|
58
|
+
const messagesValue = raw.messages;
|
|
59
|
+
if (!Array.isArray(messagesValue) || messagesValue.length === 0) {
|
|
60
|
+
return { ok: false, status: 400, reason: "messages are required" };
|
|
61
|
+
}
|
|
62
|
+
const messages = [];
|
|
63
|
+
for (const entry of messagesValue) {
|
|
64
|
+
if (!isRecord(entry)) {
|
|
65
|
+
return { ok: false, status: 400, reason: "invalid messages" };
|
|
66
|
+
}
|
|
67
|
+
const role = entry.role;
|
|
68
|
+
if (role !== "user" && role !== "assistant") {
|
|
69
|
+
return { ok: false, status: 400, reason: "unsupported message role" };
|
|
70
|
+
}
|
|
71
|
+
messages.push({ role, content: extractContentText(entry.content) });
|
|
72
|
+
}
|
|
73
|
+
const tools = [];
|
|
74
|
+
if (Array.isArray(raw.tools)) {
|
|
75
|
+
for (const tool of raw.tools) {
|
|
76
|
+
if (!isRecord(tool)) {
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
const name = asText(tool.name).trim();
|
|
80
|
+
if (!name) {
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
tools.push({
|
|
84
|
+
name,
|
|
85
|
+
description: asText(tool.description).trim() || undefined,
|
|
86
|
+
inputSchema: isRecord(tool.input_schema) ? tool.input_schema : undefined,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return {
|
|
91
|
+
ok: true,
|
|
92
|
+
model,
|
|
93
|
+
system: extractContentText(raw.system),
|
|
94
|
+
messages,
|
|
95
|
+
tools,
|
|
96
|
+
stream: raw.stream === true,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
export function buildClaudeUserPrompt(messages) {
|
|
100
|
+
if (messages.length === 1 && messages[0]?.role === "user") {
|
|
101
|
+
return messages[0].content;
|
|
102
|
+
}
|
|
103
|
+
const lines = [
|
|
104
|
+
"Continue the conversation below and produce the next assistant reply.",
|
|
105
|
+
"",
|
|
106
|
+
"Conversation:",
|
|
107
|
+
];
|
|
108
|
+
for (const message of messages) {
|
|
109
|
+
const speaker = message.role === "assistant" ? "Assistant" : "User";
|
|
110
|
+
lines.push(`${speaker}: ${message.content}`);
|
|
111
|
+
}
|
|
112
|
+
return lines.join("\n");
|
|
113
|
+
}
|
|
114
|
+
function buildToolSystemPrompt(params) {
|
|
115
|
+
const lines = [];
|
|
116
|
+
pushSection(lines, "System requirements", params.system);
|
|
117
|
+
lines.push("You are preparing the next assistant turn for an outer Anthropic-compatible orchestrator.", "Tool execution is disabled in this local Claude Code session.", "If a tool is needed, return a tool-calls object for the outer orchestrator to execute.", "If no tool is needed, return a final object with the assistant text.", "Only use the provided tool names and their declared input schema.", "Return only a single JSON object and no surrounding prose.", 'Allowed shapes:', '{"type":"final","content":"..."}', '{"type":"tool-calls","tool_calls":[{"id":"call_1","name":"toolName","arguments":{}}]}', "", "Available tools:", JSON.stringify(params.tools.map((tool) => ({
|
|
118
|
+
name: tool.name,
|
|
119
|
+
description: tool.description ?? "",
|
|
120
|
+
input_schema: tool.inputSchema ?? { type: "object", properties: {} },
|
|
121
|
+
}))));
|
|
122
|
+
return lines.join("\n").trim();
|
|
123
|
+
}
|
|
124
|
+
export function buildClaudePreparedRequest(params) {
|
|
125
|
+
const userPrompt = buildClaudeUserPrompt(params.messages);
|
|
126
|
+
if (params.tools.length > 0) {
|
|
127
|
+
return {
|
|
128
|
+
systemPrompt: buildToolSystemPrompt({ system: params.system, tools: params.tools }),
|
|
129
|
+
userPrompt,
|
|
130
|
+
outputFormat: "text",
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
return {
|
|
134
|
+
systemPrompt: params.system.trim() || undefined,
|
|
135
|
+
userPrompt,
|
|
136
|
+
outputFormat: "text",
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
export function parseClaudeResponseEnvelope(text) {
|
|
140
|
+
const trimmed = text.trim();
|
|
141
|
+
if (!trimmed) {
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
let parsed;
|
|
145
|
+
try {
|
|
146
|
+
parsed = JSON.parse(trimmed);
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
if (isRecord(parsed) && isRecord(parsed.structured_output)) {
|
|
152
|
+
parsed = parsed.structured_output;
|
|
153
|
+
}
|
|
154
|
+
if (isRecord(parsed) && typeof parsed.result === "string") {
|
|
155
|
+
return {
|
|
156
|
+
type: "final",
|
|
157
|
+
content: parsed.result,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
if (!isRecord(parsed)) {
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
if (parsed.type === "final") {
|
|
164
|
+
return {
|
|
165
|
+
type: "final",
|
|
166
|
+
content: asText(parsed.content),
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
if (parsed.type !== "tool-calls" || !Array.isArray(parsed.tool_calls)) {
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
const toolCalls = parsed.tool_calls.flatMap((entry) => {
|
|
173
|
+
if (!isRecord(entry)) {
|
|
174
|
+
return [];
|
|
175
|
+
}
|
|
176
|
+
const id = asText(entry.id).trim();
|
|
177
|
+
const name = asText(entry.name).trim();
|
|
178
|
+
const args = isRecord(entry.arguments) ? entry.arguments : null;
|
|
179
|
+
if (!id || !name || !args) {
|
|
180
|
+
return [];
|
|
181
|
+
}
|
|
182
|
+
return [{ id, name, arguments: args }];
|
|
183
|
+
});
|
|
184
|
+
return toolCalls.length > 0 ? { type: "tool-calls", tool_calls: toolCalls } : null;
|
|
185
|
+
}
|
|
186
|
+
export function buildAnthropicMessageResponse(params) {
|
|
187
|
+
return new Response(JSON.stringify({
|
|
188
|
+
id: `msg_${Date.now()}`,
|
|
189
|
+
type: "message",
|
|
190
|
+
role: "assistant",
|
|
191
|
+
model: params.model,
|
|
192
|
+
content: [{ type: "text", text: params.content }],
|
|
193
|
+
stop_reason: "end_turn",
|
|
194
|
+
stop_sequence: null,
|
|
195
|
+
usage: { input_tokens: 0, output_tokens: 0 },
|
|
196
|
+
}), {
|
|
197
|
+
status: 200,
|
|
198
|
+
headers: { "content-type": "application/json; charset=utf-8" },
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
export function buildAnthropicToolUseResponse(params) {
|
|
202
|
+
return new Response(JSON.stringify({
|
|
203
|
+
id: `msg_${Date.now()}`,
|
|
204
|
+
type: "message",
|
|
205
|
+
role: "assistant",
|
|
206
|
+
model: params.model,
|
|
207
|
+
content: params.toolCalls.map((toolCall) => ({
|
|
208
|
+
type: "tool_use",
|
|
209
|
+
id: toolCall.id,
|
|
210
|
+
name: toolCall.name,
|
|
211
|
+
input: toolCall.arguments,
|
|
212
|
+
})),
|
|
213
|
+
stop_reason: "tool_use",
|
|
214
|
+
stop_sequence: null,
|
|
215
|
+
usage: { input_tokens: 0, output_tokens: 0 },
|
|
216
|
+
}), {
|
|
217
|
+
status: 200,
|
|
218
|
+
headers: { "content-type": "application/json; charset=utf-8" },
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
export function buildAnthropicErrorResponse(status, message) {
|
|
222
|
+
return new Response(JSON.stringify({
|
|
223
|
+
type: "error",
|
|
224
|
+
error: {
|
|
225
|
+
type: status >= 500 ? "api_error" : "invalid_request_error",
|
|
226
|
+
message,
|
|
227
|
+
},
|
|
228
|
+
}), {
|
|
229
|
+
status,
|
|
230
|
+
headers: { "content-type": "application/json; charset=utf-8" },
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
function encodeSSEEvent(event, payload) {
|
|
234
|
+
return `event: ${event}\ndata: ${JSON.stringify(payload)}\n\n`;
|
|
235
|
+
}
|
|
236
|
+
function buildSSEHeaders() {
|
|
237
|
+
return {
|
|
238
|
+
"content-type": "text/event-stream; charset=utf-8",
|
|
239
|
+
"cache-control": "no-cache",
|
|
240
|
+
connection: "keep-alive",
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
export function buildAnthropicMessageStreamResponse(params) {
|
|
244
|
+
const messageId = `msg_${Date.now()}`;
|
|
245
|
+
const body = [
|
|
246
|
+
encodeSSEEvent("message_start", {
|
|
247
|
+
type: "message_start",
|
|
248
|
+
message: {
|
|
249
|
+
id: messageId,
|
|
250
|
+
type: "message",
|
|
251
|
+
role: "assistant",
|
|
252
|
+
model: params.model,
|
|
253
|
+
content: [],
|
|
254
|
+
stop_reason: null,
|
|
255
|
+
stop_sequence: null,
|
|
256
|
+
usage: { input_tokens: 0, output_tokens: 0 },
|
|
257
|
+
},
|
|
258
|
+
}),
|
|
259
|
+
encodeSSEEvent("content_block_start", {
|
|
260
|
+
type: "content_block_start",
|
|
261
|
+
index: 0,
|
|
262
|
+
content_block: {
|
|
263
|
+
type: "text",
|
|
264
|
+
text: "",
|
|
265
|
+
},
|
|
266
|
+
}),
|
|
267
|
+
encodeSSEEvent("content_block_delta", {
|
|
268
|
+
type: "content_block_delta",
|
|
269
|
+
index: 0,
|
|
270
|
+
delta: {
|
|
271
|
+
type: "text_delta",
|
|
272
|
+
text: params.content,
|
|
273
|
+
},
|
|
274
|
+
}),
|
|
275
|
+
encodeSSEEvent("content_block_stop", {
|
|
276
|
+
type: "content_block_stop",
|
|
277
|
+
index: 0,
|
|
278
|
+
}),
|
|
279
|
+
encodeSSEEvent("message_delta", {
|
|
280
|
+
type: "message_delta",
|
|
281
|
+
delta: {
|
|
282
|
+
stop_reason: "end_turn",
|
|
283
|
+
stop_sequence: null,
|
|
284
|
+
},
|
|
285
|
+
usage: { output_tokens: 0 },
|
|
286
|
+
}),
|
|
287
|
+
encodeSSEEvent("message_stop", {
|
|
288
|
+
type: "message_stop",
|
|
289
|
+
}),
|
|
290
|
+
].join("");
|
|
291
|
+
return new Response(body, {
|
|
292
|
+
status: 200,
|
|
293
|
+
headers: buildSSEHeaders(),
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
export function buildAnthropicToolUseStreamResponse(params) {
|
|
297
|
+
const messageId = `msg_${Date.now()}`;
|
|
298
|
+
const bodyParts = [
|
|
299
|
+
encodeSSEEvent("message_start", {
|
|
300
|
+
type: "message_start",
|
|
301
|
+
message: {
|
|
302
|
+
id: messageId,
|
|
303
|
+
type: "message",
|
|
304
|
+
role: "assistant",
|
|
305
|
+
model: params.model,
|
|
306
|
+
content: [],
|
|
307
|
+
stop_reason: null,
|
|
308
|
+
stop_sequence: null,
|
|
309
|
+
usage: { input_tokens: 0, output_tokens: 0 },
|
|
310
|
+
},
|
|
311
|
+
}),
|
|
312
|
+
];
|
|
313
|
+
params.toolCalls.forEach((toolCall, index) => {
|
|
314
|
+
bodyParts.push(encodeSSEEvent("content_block_start", {
|
|
315
|
+
type: "content_block_start",
|
|
316
|
+
index,
|
|
317
|
+
content_block: {
|
|
318
|
+
type: "tool_use",
|
|
319
|
+
id: toolCall.id,
|
|
320
|
+
name: toolCall.name,
|
|
321
|
+
input: toolCall.arguments,
|
|
322
|
+
},
|
|
323
|
+
}), encodeSSEEvent("content_block_stop", {
|
|
324
|
+
type: "content_block_stop",
|
|
325
|
+
index,
|
|
326
|
+
}));
|
|
327
|
+
});
|
|
328
|
+
bodyParts.push(encodeSSEEvent("message_delta", {
|
|
329
|
+
type: "message_delta",
|
|
330
|
+
delta: {
|
|
331
|
+
stop_reason: "tool_use",
|
|
332
|
+
stop_sequence: null,
|
|
333
|
+
},
|
|
334
|
+
usage: { output_tokens: 0 },
|
|
335
|
+
}), encodeSSEEvent("message_stop", {
|
|
336
|
+
type: "message_stop",
|
|
337
|
+
}));
|
|
338
|
+
return new Response(bodyParts.join(""), {
|
|
339
|
+
status: 200,
|
|
340
|
+
headers: buildSSEHeaders(),
|
|
341
|
+
});
|
|
342
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export type ClaudeCodeRunResult = {
|
|
2
|
+
ok: boolean;
|
|
3
|
+
responseText?: string;
|
|
4
|
+
reason?: string;
|
|
5
|
+
exitCode?: number;
|
|
6
|
+
stdout?: string;
|
|
7
|
+
stderr?: string;
|
|
8
|
+
};
|
|
9
|
+
export declare function runClaudeCode(params: {
|
|
10
|
+
prompt: string;
|
|
11
|
+
systemPrompt?: string;
|
|
12
|
+
model?: string;
|
|
13
|
+
effort?: "low" | "medium" | "high";
|
|
14
|
+
outputFormat?: "text" | "json" | "stream-json";
|
|
15
|
+
jsonSchema?: Record<string, unknown>;
|
|
16
|
+
verbose?: boolean;
|
|
17
|
+
cwd?: string;
|
|
18
|
+
env?: NodeJS.ProcessEnv;
|
|
19
|
+
timeoutMs?: number;
|
|
20
|
+
maxOutputChars?: number;
|
|
21
|
+
}): Promise<ClaudeCodeRunResult>;
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import { spawn as spawnNode } from "node:child_process";
|
|
2
|
+
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { extname, resolve } from "node:path";
|
|
5
|
+
function nonEmpty(value) {
|
|
6
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
7
|
+
}
|
|
8
|
+
function truncate(text, maxChars) {
|
|
9
|
+
if (text.length <= maxChars) {
|
|
10
|
+
return text;
|
|
11
|
+
}
|
|
12
|
+
return text.slice(0, maxChars);
|
|
13
|
+
}
|
|
14
|
+
function summarizeProcessFailure(stdout, stderr) {
|
|
15
|
+
const detail = [stderr.trim(), stdout.trim()].filter(Boolean).join(" | ");
|
|
16
|
+
return detail ? truncate(detail, 400) : "no process output";
|
|
17
|
+
}
|
|
18
|
+
async function createTempTextFile(prefix, fileName, text) {
|
|
19
|
+
const dir = await mkdtemp(resolve(tmpdir(), prefix));
|
|
20
|
+
const path = resolve(dir, fileName);
|
|
21
|
+
await writeFile(path, text, "utf-8");
|
|
22
|
+
return {
|
|
23
|
+
path,
|
|
24
|
+
cleanup: async () => {
|
|
25
|
+
await rm(dir, { recursive: true, force: true });
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
function parseHelpCapabilities(helpText) {
|
|
30
|
+
const text = helpText || "";
|
|
31
|
+
const lines = text.split(/\r?\n/);
|
|
32
|
+
const permissionModeLine = lines.find((line) => /\B--permission-mode\b/i.test(line)) ?? "";
|
|
33
|
+
return {
|
|
34
|
+
hasPrintFlag: /(^|\s)-p(\s|,|$)|\B--print\b/i.test(text),
|
|
35
|
+
hasOutputFormat: /\B--output-format\b/i.test(text),
|
|
36
|
+
hasPermissionMode: /\B--permission-mode\b/i.test(text),
|
|
37
|
+
hasPermissionModePlanSupport: /\bplan\b/i.test(permissionModeLine),
|
|
38
|
+
hasToolsFlag: /\B--tools\b/i.test(text),
|
|
39
|
+
hasNoSessionPersistenceFlag: /\B--no-session-persistence\b/i.test(text),
|
|
40
|
+
hasModelFlag: /\B--model\b/i.test(text),
|
|
41
|
+
hasEffortFlag: /\B--effort\b/i.test(text),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
function normalizeEffort(value) {
|
|
45
|
+
if (!nonEmpty(value)) {
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
const normalized = value.trim().toLowerCase();
|
|
49
|
+
if (normalized === "low" || normalized === "medium" || normalized === "high") {
|
|
50
|
+
return normalized;
|
|
51
|
+
}
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
function resolveClaudeModelAlias(model) {
|
|
55
|
+
if (!nonEmpty(model)) {
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
const normalized = model.trim().toLowerCase();
|
|
59
|
+
if (normalized.startsWith("anthropic/")) {
|
|
60
|
+
return resolveClaudeModelAlias(normalized.slice("anthropic/".length));
|
|
61
|
+
}
|
|
62
|
+
if (normalized.startsWith("claude-opus")) {
|
|
63
|
+
return "opus";
|
|
64
|
+
}
|
|
65
|
+
if (normalized.startsWith("claude-haiku")) {
|
|
66
|
+
return "haiku";
|
|
67
|
+
}
|
|
68
|
+
if (normalized.startsWith("claude-sonnet")) {
|
|
69
|
+
return "sonnet";
|
|
70
|
+
}
|
|
71
|
+
return model.trim();
|
|
72
|
+
}
|
|
73
|
+
async function collectStream(stream, maxChars) {
|
|
74
|
+
if (!stream) {
|
|
75
|
+
return "";
|
|
76
|
+
}
|
|
77
|
+
const chunks = [];
|
|
78
|
+
let total = 0;
|
|
79
|
+
try {
|
|
80
|
+
for await (const chunk of stream) {
|
|
81
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
82
|
+
chunks.push(buffer);
|
|
83
|
+
total += buffer.length;
|
|
84
|
+
if (total >= maxChars * 2) {
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
const typed = error;
|
|
91
|
+
if (typed.code !== "ERR_STREAM_PREMATURE_CLOSE") {
|
|
92
|
+
throw error;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return truncate(Buffer.concat(chunks).toString("utf-8"), maxChars);
|
|
96
|
+
}
|
|
97
|
+
async function spawnAndCollect(params) {
|
|
98
|
+
const isWindows = process.platform === "win32";
|
|
99
|
+
const ext = extname(params.bin).toLowerCase();
|
|
100
|
+
const shouldWrapNode = isWindows && (ext === ".js" || ext === ".mjs" || ext === ".cjs");
|
|
101
|
+
const actualBin = shouldWrapNode ? "node" : params.bin;
|
|
102
|
+
const actualArgs = shouldWrapNode ? [params.bin, ...params.args] : params.args;
|
|
103
|
+
let child;
|
|
104
|
+
try {
|
|
105
|
+
child = spawnNode(actualBin, actualArgs, {
|
|
106
|
+
cwd: params.cwd,
|
|
107
|
+
env: {
|
|
108
|
+
...params.env,
|
|
109
|
+
CI: "true",
|
|
110
|
+
NO_COLOR: "1",
|
|
111
|
+
TERM: "dumb",
|
|
112
|
+
},
|
|
113
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
const typed = error;
|
|
118
|
+
return {
|
|
119
|
+
exitCode: 127,
|
|
120
|
+
stdout: "",
|
|
121
|
+
stderr: typed.message || String(error),
|
|
122
|
+
timedOut: false,
|
|
123
|
+
spawnErrorCode: typeof typed.code === "string" ? typed.code : undefined,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
if (typeof params.stdinText === "string") {
|
|
127
|
+
child.stdin?.end(params.stdinText);
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
child.stdin?.end();
|
|
131
|
+
}
|
|
132
|
+
let timedOut = false;
|
|
133
|
+
let spawnErrorCode;
|
|
134
|
+
child.once("error", (error) => {
|
|
135
|
+
const typed = error;
|
|
136
|
+
spawnErrorCode = typeof typed.code === "string" ? typed.code : undefined;
|
|
137
|
+
child.stdout?.destroy();
|
|
138
|
+
child.stderr?.destroy();
|
|
139
|
+
});
|
|
140
|
+
const timer = setTimeout(() => {
|
|
141
|
+
timedOut = true;
|
|
142
|
+
if (!child.killed) {
|
|
143
|
+
child.kill();
|
|
144
|
+
}
|
|
145
|
+
}, Math.max(100, params.timeoutMs));
|
|
146
|
+
const exitCodePromise = new Promise((resolveExit) => {
|
|
147
|
+
child.once("close", (code) => resolveExit(typeof code === "number" ? code : 1));
|
|
148
|
+
child.once("error", () => resolveExit(127));
|
|
149
|
+
});
|
|
150
|
+
const [stdout, stderr, exitCode] = await Promise.all([
|
|
151
|
+
collectStream(child.stdout, params.maxOutputChars),
|
|
152
|
+
collectStream(child.stderr, params.maxOutputChars),
|
|
153
|
+
exitCodePromise,
|
|
154
|
+
]);
|
|
155
|
+
clearTimeout(timer);
|
|
156
|
+
return { exitCode, stdout, stderr, timedOut, spawnErrorCode };
|
|
157
|
+
}
|
|
158
|
+
export async function runClaudeCode(params) {
|
|
159
|
+
const prompt = nonEmpty(params.prompt) ? params.prompt.trim() : "";
|
|
160
|
+
if (!prompt) {
|
|
161
|
+
return { ok: false, reason: "prompt is required" };
|
|
162
|
+
}
|
|
163
|
+
const env = params.env ?? process.env;
|
|
164
|
+
const bin = nonEmpty(env.OPENCODE_CLAUDE_AUTH_BIN)
|
|
165
|
+
? env.OPENCODE_CLAUDE_AUTH_BIN.trim()
|
|
166
|
+
: nonEmpty(env.AEGIS_CLAUDE_CODE_CLI_BIN)
|
|
167
|
+
? env.AEGIS_CLAUDE_CODE_CLI_BIN.trim()
|
|
168
|
+
: "claude";
|
|
169
|
+
const cwd = nonEmpty(params.cwd) ? resolve(params.cwd) : process.cwd();
|
|
170
|
+
const timeoutMs = typeof params.timeoutMs === "number" ? Math.max(100, Math.floor(params.timeoutMs)) : 60_000;
|
|
171
|
+
const maxOutputChars = typeof params.maxOutputChars === "number" ? Math.max(1_000, Math.floor(params.maxOutputChars)) : 20_000;
|
|
172
|
+
const outputFormat = params.outputFormat ?? "text";
|
|
173
|
+
const help = await spawnAndCollect({
|
|
174
|
+
bin,
|
|
175
|
+
args: ["--help"],
|
|
176
|
+
cwd,
|
|
177
|
+
env,
|
|
178
|
+
timeoutMs: Math.min(timeoutMs, 10_000),
|
|
179
|
+
maxOutputChars,
|
|
180
|
+
});
|
|
181
|
+
if (help.timedOut) {
|
|
182
|
+
return { ok: false, reason: "claude --help timed out", exitCode: 124, stdout: help.stdout, stderr: help.stderr };
|
|
183
|
+
}
|
|
184
|
+
if (help.spawnErrorCode === "ENOENT") {
|
|
185
|
+
return { ok: false, reason: `Claude Code CLI binary not found: ${bin}`, exitCode: 127 };
|
|
186
|
+
}
|
|
187
|
+
if (help.exitCode !== 0) {
|
|
188
|
+
return {
|
|
189
|
+
ok: false,
|
|
190
|
+
reason: `claude --help failed (exit=${help.exitCode}): ${summarizeProcessFailure(help.stdout, help.stderr)}`,
|
|
191
|
+
exitCode: help.exitCode,
|
|
192
|
+
stdout: help.stdout,
|
|
193
|
+
stderr: help.stderr,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
const capabilities = parseHelpCapabilities(`${help.stdout}\n${help.stderr}`);
|
|
197
|
+
const missing = [];
|
|
198
|
+
if (!capabilities.hasPrintFlag)
|
|
199
|
+
missing.push("-p/--print");
|
|
200
|
+
if (!capabilities.hasOutputFormat)
|
|
201
|
+
missing.push("--output-format");
|
|
202
|
+
if (!capabilities.hasPermissionMode)
|
|
203
|
+
missing.push("--permission-mode");
|
|
204
|
+
if (!capabilities.hasPermissionModePlanSupport)
|
|
205
|
+
missing.push("--permission-mode plan");
|
|
206
|
+
if (!capabilities.hasToolsFlag)
|
|
207
|
+
missing.push("--tools");
|
|
208
|
+
if (!capabilities.hasNoSessionPersistenceFlag)
|
|
209
|
+
missing.push("--no-session-persistence");
|
|
210
|
+
if (missing.length > 0) {
|
|
211
|
+
return { ok: false, reason: `Claude Code CLI is missing required safe flags: ${missing.join(", ")}` };
|
|
212
|
+
}
|
|
213
|
+
const args = [
|
|
214
|
+
"-p",
|
|
215
|
+
"--output-format",
|
|
216
|
+
outputFormat,
|
|
217
|
+
"--permission-mode",
|
|
218
|
+
"plan",
|
|
219
|
+
"--tools",
|
|
220
|
+
"",
|
|
221
|
+
"--no-session-persistence",
|
|
222
|
+
];
|
|
223
|
+
if (params.verbose) {
|
|
224
|
+
args.push("--verbose");
|
|
225
|
+
}
|
|
226
|
+
let systemPromptFile;
|
|
227
|
+
if (nonEmpty(params.systemPrompt)) {
|
|
228
|
+
systemPromptFile = await createTempTextFile("opencode-claude-system-", "system-prompt.txt", params.systemPrompt.trim());
|
|
229
|
+
args.push("--system-prompt-file", systemPromptFile.path);
|
|
230
|
+
}
|
|
231
|
+
if (params.jsonSchema && typeof params.jsonSchema === "object") {
|
|
232
|
+
args.push("--json-schema", JSON.stringify(params.jsonSchema));
|
|
233
|
+
}
|
|
234
|
+
const model = resolveClaudeModelAlias(params.model);
|
|
235
|
+
if (model && capabilities.hasModelFlag) {
|
|
236
|
+
args.push("--model", model);
|
|
237
|
+
}
|
|
238
|
+
const effort = normalizeEffort(params.effort);
|
|
239
|
+
if (effort && capabilities.hasEffortFlag) {
|
|
240
|
+
args.push("--effort", effort);
|
|
241
|
+
}
|
|
242
|
+
const run = await (async () => {
|
|
243
|
+
try {
|
|
244
|
+
return await spawnAndCollect({
|
|
245
|
+
bin,
|
|
246
|
+
args,
|
|
247
|
+
cwd,
|
|
248
|
+
env,
|
|
249
|
+
timeoutMs,
|
|
250
|
+
maxOutputChars,
|
|
251
|
+
stdinText: prompt,
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
finally {
|
|
255
|
+
await systemPromptFile?.cleanup();
|
|
256
|
+
}
|
|
257
|
+
})();
|
|
258
|
+
if (run.timedOut) {
|
|
259
|
+
return { ok: false, reason: `Claude Code CLI timed out after ${timeoutMs}ms`, exitCode: 124, stdout: run.stdout, stderr: run.stderr };
|
|
260
|
+
}
|
|
261
|
+
if (run.spawnErrorCode === "ENOENT") {
|
|
262
|
+
return { ok: false, reason: `Claude Code CLI binary not found: ${bin}`, exitCode: 127 };
|
|
263
|
+
}
|
|
264
|
+
if (run.exitCode !== 0) {
|
|
265
|
+
return {
|
|
266
|
+
ok: false,
|
|
267
|
+
reason: `claude exited with code ${run.exitCode}: ${summarizeProcessFailure(run.stdout, run.stderr)}`,
|
|
268
|
+
exitCode: run.exitCode,
|
|
269
|
+
stdout: run.stdout,
|
|
270
|
+
stderr: run.stderr,
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
return { ok: true, responseText: run.stdout.trim(), exitCode: 0, stdout: run.stdout, stderr: run.stderr };
|
|
274
|
+
}
|
package/dist/fetch.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { runClaudeCode } from "./claude-cli.js";
|
|
2
|
+
export type ClaudeCodeFetchDeps = {
|
|
3
|
+
runClaudeCodeImpl?: typeof runClaudeCode;
|
|
4
|
+
};
|
|
5
|
+
export declare function createClaudeCodeFetch(deps?: ClaudeCodeFetchDeps): (_input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
|
package/dist/fetch.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { buildAnthropicErrorResponse, buildAnthropicMessageResponse, buildAnthropicMessageStreamResponse, buildAnthropicToolUseResponse, buildAnthropicToolUseStreamResponse, buildClaudePreparedRequest, extractAnthropicRequest, parseClaudeResponseEnvelope, } from "./anthropic-compat.js";
|
|
2
|
+
import { runClaudeCode } from "./claude-cli.js";
|
|
3
|
+
function effortFromRequest(_raw) {
|
|
4
|
+
return undefined;
|
|
5
|
+
}
|
|
6
|
+
function errorFromRunResult(result) {
|
|
7
|
+
const status = result.exitCode === 124 ? 504 : 502;
|
|
8
|
+
return buildAnthropicErrorResponse(status, result.reason ?? "Claude Code CLI failed");
|
|
9
|
+
}
|
|
10
|
+
export function createClaudeCodeFetch(deps = {}) {
|
|
11
|
+
const runClaudeCodeImpl = deps.runClaudeCodeImpl ?? runClaudeCode;
|
|
12
|
+
return async (_input, init) => {
|
|
13
|
+
if (init?.body !== undefined && typeof init.body !== "string") {
|
|
14
|
+
return buildAnthropicErrorResponse(400, "request body must be a JSON string");
|
|
15
|
+
}
|
|
16
|
+
if (typeof init?.body !== "string" || !init.body.trim()) {
|
|
17
|
+
return buildAnthropicErrorResponse(400, "request body is required");
|
|
18
|
+
}
|
|
19
|
+
let parsed;
|
|
20
|
+
try {
|
|
21
|
+
parsed = JSON.parse(init.body);
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return buildAnthropicErrorResponse(400, "request body must be valid JSON");
|
|
25
|
+
}
|
|
26
|
+
const request = extractAnthropicRequest(parsed);
|
|
27
|
+
if (!request.ok) {
|
|
28
|
+
return buildAnthropicErrorResponse(request.status, request.reason);
|
|
29
|
+
}
|
|
30
|
+
const prepared = buildClaudePreparedRequest({
|
|
31
|
+
system: request.system,
|
|
32
|
+
messages: request.messages,
|
|
33
|
+
tools: request.tools,
|
|
34
|
+
});
|
|
35
|
+
const result = await runClaudeCodeImpl({
|
|
36
|
+
prompt: prepared.userPrompt,
|
|
37
|
+
systemPrompt: prepared.systemPrompt,
|
|
38
|
+
outputFormat: prepared.outputFormat,
|
|
39
|
+
model: request.model,
|
|
40
|
+
effort: effortFromRequest(parsed),
|
|
41
|
+
});
|
|
42
|
+
if (!result.ok) {
|
|
43
|
+
return errorFromRunResult(result);
|
|
44
|
+
}
|
|
45
|
+
const responseText = result.responseText ?? "";
|
|
46
|
+
const envelope = parseClaudeResponseEnvelope(responseText);
|
|
47
|
+
if (envelope?.type === "tool-calls") {
|
|
48
|
+
if (request.stream) {
|
|
49
|
+
return buildAnthropicToolUseStreamResponse({
|
|
50
|
+
model: request.model,
|
|
51
|
+
toolCalls: envelope.tool_calls,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
return buildAnthropicToolUseResponse({
|
|
55
|
+
model: request.model,
|
|
56
|
+
toolCalls: envelope.tool_calls,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
if (request.stream) {
|
|
60
|
+
return buildAnthropicMessageStreamResponse({
|
|
61
|
+
model: request.model,
|
|
62
|
+
content: envelope?.type === "final" ? envelope.content : responseText,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
return buildAnthropicMessageResponse({
|
|
66
|
+
model: request.model,
|
|
67
|
+
content: envelope?.type === "final" ? envelope.content : responseText,
|
|
68
|
+
});
|
|
69
|
+
};
|
|
70
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export type AuthMethod = {
|
|
2
|
+
provider?: string;
|
|
3
|
+
label: string;
|
|
4
|
+
type: "api" | "oauth";
|
|
5
|
+
};
|
|
6
|
+
export type LoaderResult = {
|
|
7
|
+
apiKey: string;
|
|
8
|
+
fetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response>;
|
|
9
|
+
};
|
|
10
|
+
export type AuthHook = {
|
|
11
|
+
provider: string;
|
|
12
|
+
loader(getAuth?: unknown, provider?: unknown): Promise<LoaderResult | null>;
|
|
13
|
+
methods: AuthMethod[];
|
|
14
|
+
};
|
|
15
|
+
export type ClaudeAuthPlugin = {
|
|
16
|
+
auth: AuthHook;
|
|
17
|
+
};
|
|
18
|
+
type PluginContext = {
|
|
19
|
+
client?: {
|
|
20
|
+
auth?: {
|
|
21
|
+
set(input: {
|
|
22
|
+
path: {
|
|
23
|
+
id: string;
|
|
24
|
+
};
|
|
25
|
+
body: {
|
|
26
|
+
type: "api";
|
|
27
|
+
key: string;
|
|
28
|
+
apiKey: string;
|
|
29
|
+
};
|
|
30
|
+
}): Promise<unknown>;
|
|
31
|
+
};
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
declare function ClaudeCodeCLIAuthPlugin(context?: PluginContext): Promise<ClaudeAuthPlugin>;
|
|
35
|
+
export default ClaudeCodeCLIAuthPlugin;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { createClaudeCodeFetch } from "./fetch.js";
|
|
2
|
+
const CLAUDE_PROVIDER_ID = "anthropic";
|
|
3
|
+
async function seedLocalAnthropicAuth(context) {
|
|
4
|
+
const auth = context?.client?.auth;
|
|
5
|
+
if (!auth?.set) {
|
|
6
|
+
return;
|
|
7
|
+
}
|
|
8
|
+
await auth.set({
|
|
9
|
+
path: { id: CLAUDE_PROVIDER_ID },
|
|
10
|
+
body: {
|
|
11
|
+
type: "api",
|
|
12
|
+
key: "claude-code-cli",
|
|
13
|
+
apiKey: "claude-code-cli",
|
|
14
|
+
},
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
async function ClaudeCodeCLIAuthPlugin(context) {
|
|
18
|
+
await seedLocalAnthropicAuth(context);
|
|
19
|
+
return {
|
|
20
|
+
auth: {
|
|
21
|
+
provider: CLAUDE_PROVIDER_ID,
|
|
22
|
+
loader: async () => ({
|
|
23
|
+
apiKey: "claude-code-cli",
|
|
24
|
+
fetch: createClaudeCodeFetch(),
|
|
25
|
+
}),
|
|
26
|
+
methods: [
|
|
27
|
+
{
|
|
28
|
+
provider: CLAUDE_PROVIDER_ID,
|
|
29
|
+
label: "Claude Code CLI (local)",
|
|
30
|
+
type: "api",
|
|
31
|
+
},
|
|
32
|
+
],
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
export default ClaudeCodeCLIAuthPlugin;
|
package/dist/smoke.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/smoke.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import ClaudeCodeCLIAuthPlugin from "./index.js";
|
|
2
|
+
async function main() {
|
|
3
|
+
const args = process.argv.slice(2);
|
|
4
|
+
const stream = args.includes("--stream");
|
|
5
|
+
const prompt = args.filter((arg) => arg !== "--stream").join(" ").trim() || "Reply with exactly SMOKE_OK";
|
|
6
|
+
const plugin = await ClaudeCodeCLIAuthPlugin();
|
|
7
|
+
const loader = await plugin.auth.loader();
|
|
8
|
+
if (!loader) {
|
|
9
|
+
throw new Error("auth loader returned null");
|
|
10
|
+
}
|
|
11
|
+
const response = await loader.fetch("https://api.anthropic.com/v1/messages", {
|
|
12
|
+
method: "POST",
|
|
13
|
+
headers: { "content-type": "application/json" },
|
|
14
|
+
body: JSON.stringify({
|
|
15
|
+
model: "claude-sonnet-4-5",
|
|
16
|
+
max_tokens: 128,
|
|
17
|
+
stream,
|
|
18
|
+
messages: [{ role: "user", content: prompt }],
|
|
19
|
+
}),
|
|
20
|
+
});
|
|
21
|
+
const bodyText = await response.text();
|
|
22
|
+
process.stdout.write(`${bodyText}\n`);
|
|
23
|
+
if (!response.ok) {
|
|
24
|
+
process.exitCode = 1;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
void main();
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "opencode-cluade-auth",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Claude Code CLI-backed Anthropic auth plugin for OpenCode.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"sideEffects": false,
|
|
9
|
+
"keywords": [
|
|
10
|
+
"anthropic",
|
|
11
|
+
"claude",
|
|
12
|
+
"claude-code",
|
|
13
|
+
"opencode",
|
|
14
|
+
"plugin",
|
|
15
|
+
"auth"
|
|
16
|
+
],
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "git+https://github.com/GunP4ng/openocde-claude-auth.git"
|
|
20
|
+
},
|
|
21
|
+
"homepage": "https://github.com/GunP4ng/openocde-claude-auth#readme",
|
|
22
|
+
"bugs": {
|
|
23
|
+
"url": "https://github.com/GunP4ng/openocde-claude-auth/issues"
|
|
24
|
+
},
|
|
25
|
+
"exports": {
|
|
26
|
+
".": {
|
|
27
|
+
"types": "./dist/index.d.ts",
|
|
28
|
+
"default": "./dist/index.js"
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"files": [
|
|
32
|
+
"dist",
|
|
33
|
+
"README.md"
|
|
34
|
+
],
|
|
35
|
+
"scripts": {
|
|
36
|
+
"build": "tsc -p tsconfig.build.json",
|
|
37
|
+
"typecheck": "tsc -p tsconfig.json",
|
|
38
|
+
"test": "bun test",
|
|
39
|
+
"smoke": "bun run src/smoke.ts",
|
|
40
|
+
"prepublishOnly": "npm run build"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@types/bun": "latest",
|
|
44
|
+
"@types/node": "^22.10.2",
|
|
45
|
+
"typescript": "^5.5.4"
|
|
46
|
+
}
|
|
47
|
+
}
|