@semanticist14/clco 0.1.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/LICENSE +21 -0
- package/README.md +279 -0
- package/bin/clco +27 -0
- package/bun.lock +39 -0
- package/install.sh +161 -0
- package/package.json +35 -0
- package/scripts/mock-upstream.ts +129 -0
- package/scripts/write-launcher.sh +47 -0
- package/src/api.ts +90 -0
- package/src/auth.ts +130 -0
- package/src/blocks.ts +198 -0
- package/src/browsermcp.ts +430 -0
- package/src/catalog.ts +147 -0
- package/src/claudehome.ts +269 -0
- package/src/cli.ts +889 -0
- package/src/config.ts +164 -0
- package/src/responses.ts +435 -0
- package/src/route.ts +52 -0
- package/src/server.ts +641 -0
- package/src/setup.ts +218 -0
- package/src/spawn.ts +453 -0
- package/src/stream.ts +235 -0
- package/src/tls.ts +234 -0
- package/src/token.ts +298 -0
- package/src/tokens.ts +55 -0
- package/src/translate.ts +384 -0
- package/src/wire.ts +149 -0
- package/uninstall.sh +42 -0
package/src/translate.ts
ADDED
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
// Translation between the Anthropic Messages API (what Claude Code speaks)
|
|
2
|
+
// and GitHub Copilot's OpenAI-style chat/completions API.
|
|
3
|
+
//
|
|
4
|
+
// Content classification lives in blocks.ts; wire types in wire.ts;
|
|
5
|
+
// streaming response state lives in stream.ts.
|
|
6
|
+
|
|
7
|
+
import type {
|
|
8
|
+
CacheControl,
|
|
9
|
+
AnthropicMessage,
|
|
10
|
+
AnthropicRequest,
|
|
11
|
+
OpenAITextPart,
|
|
12
|
+
OpenAIImagePart,
|
|
13
|
+
OpenAIMessage,
|
|
14
|
+
OpenAIRequest,
|
|
15
|
+
OpenAIUsage,
|
|
16
|
+
OpenAIChoice,
|
|
17
|
+
OpenAIResponse,
|
|
18
|
+
AnthropicContentBlock,
|
|
19
|
+
AnthropicResponse,
|
|
20
|
+
} from "./wire"
|
|
21
|
+
import {
|
|
22
|
+
type ImageBlock,
|
|
23
|
+
type ToolResultBlock,
|
|
24
|
+
type ToolUseBlock,
|
|
25
|
+
classifyContent,
|
|
26
|
+
} from "./blocks"
|
|
27
|
+
|
|
28
|
+
function marked(blocks: unknown): boolean {
|
|
29
|
+
return (
|
|
30
|
+
Array.isArray(blocks) &&
|
|
31
|
+
blocks.some(
|
|
32
|
+
(b) => b && typeof b === "object" && (b as CacheControl).cache_control,
|
|
33
|
+
)
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Copilot's OpenAI-compatible surface spells the prompt-cache marker
|
|
38
|
+
// `copilot_cache_control` rather than the standard `cache_control`.
|
|
39
|
+
const COPILOT_CACHE = { type: "ephemeral" } as const
|
|
40
|
+
|
|
41
|
+
// Copilot declares the reasoning_effort values each model accepts in its
|
|
42
|
+
// /models capabilities. Send the effort only when the model claims it —
|
|
43
|
+
// anything else is dropped rather than risking a 400.
|
|
44
|
+
export function effortFor(
|
|
45
|
+
payload: AnthropicRequest,
|
|
46
|
+
allowed: string[] | null | undefined,
|
|
47
|
+
): string | undefined {
|
|
48
|
+
const effort = payload.output_config?.effort
|
|
49
|
+
if (!effort || !allowed || !allowed.includes(effort)) return undefined
|
|
50
|
+
return effort
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
// Request direction: Anthropic -> OpenAI
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
|
|
57
|
+
// Ids clco advertised to Claude Code, mapped back to the upstream slug they
|
|
58
|
+
// stand for. Populated from the live /models response after discovery, so the
|
|
59
|
+
// table always matches what the picker actually offered. Kept here rather
|
|
60
|
+
// than read from token.ts to avoid a cycle.
|
|
61
|
+
let modelAliases = new Map<string, string>()
|
|
62
|
+
|
|
63
|
+
export function setModelAliases(aliases: Map<string, string>): void {
|
|
64
|
+
modelAliases = aliases
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Copilot slugs are dot-form ("claude-sonnet-4.5"). Claude Code may echo back
|
|
68
|
+
// an advertised alias, dash-form, a date suffix, or a bracket suffix like
|
|
69
|
+
// [1m]; normalize all. The alias table wins over the pattern rules because it
|
|
70
|
+
// is derived from the upstream's own list — the patterns are only a fallback
|
|
71
|
+
// for ids discovery never saw (and they miss families like fable entirely).
|
|
72
|
+
export function normalizeModel(model: string): string {
|
|
73
|
+
let m = model.replace(/\[[^\]]*\]$/, "")
|
|
74
|
+
m = m.replace(/-\d{8}$/, "")
|
|
75
|
+
const alias = modelAliases.get(m)
|
|
76
|
+
if (alias) return alias
|
|
77
|
+
m = m.replace(
|
|
78
|
+
/^claude-(opus|sonnet|haiku)-(\d+)-(\d+)$/,
|
|
79
|
+
"claude-$1-$2.$3",
|
|
80
|
+
)
|
|
81
|
+
m = m.replace(/^claude-(\d+)-(\d+)-(sonnet|haiku|opus)$/, "claude-$1.$2-$3")
|
|
82
|
+
return m
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function debugWarn(message: string): void {
|
|
86
|
+
if (process.env.CLCO_DEBUG) console.error("[clco:debug]", message)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function toImagePart(block: ImageBlock): OpenAIImagePart {
|
|
90
|
+
return {
|
|
91
|
+
type: "image_url",
|
|
92
|
+
image_url: {
|
|
93
|
+
url: `data:${block.source.media_type};base64,${block.source.data}`,
|
|
94
|
+
},
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Tool messages must carry string content (OpenAI schema); images are moved
|
|
99
|
+
// into the adjacent user message instead, otherwise one image tool_result
|
|
100
|
+
// would poison every subsequent request in the session.
|
|
101
|
+
function toolResultText(result: ToolResultBlock): {
|
|
102
|
+
text: string
|
|
103
|
+
images: OpenAIImagePart[]
|
|
104
|
+
} {
|
|
105
|
+
const images: OpenAIImagePart[] = []
|
|
106
|
+
let text: string
|
|
107
|
+
if (typeof result.content === "string") {
|
|
108
|
+
text = result.content
|
|
109
|
+
} else if (Array.isArray(result.content)) {
|
|
110
|
+
const texts: string[] = []
|
|
111
|
+
for (const view of classifyContent(result.content)) {
|
|
112
|
+
if (view.kind === "text") texts.push(view.text)
|
|
113
|
+
else if (view.kind === "image") images.push(toImagePart(view.block))
|
|
114
|
+
// A tool_result nesting a tool_use or a document is not something the
|
|
115
|
+
// protocol produces, but saying so beats dropping it.
|
|
116
|
+
else if (view.kind === "unsupported") {
|
|
117
|
+
texts.push(view.text)
|
|
118
|
+
debugWarn(`tool_result carried a ${view.blockType} block`)
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
text = texts.filter(Boolean).join("\n\n")
|
|
122
|
+
} else {
|
|
123
|
+
text = ""
|
|
124
|
+
}
|
|
125
|
+
if (result.is_error) text = `[error] ${text}`
|
|
126
|
+
if (images.length > 0) {
|
|
127
|
+
text =
|
|
128
|
+
(text ? `${text}\n\n` : "") +
|
|
129
|
+
`[${images.length} image(s) - attached to the next user message]`
|
|
130
|
+
}
|
|
131
|
+
return { text, images }
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function translateUserMessage(message: AnthropicMessage): OpenAIMessage[] {
|
|
135
|
+
if (typeof message.content === "string") {
|
|
136
|
+
return [{ role: "user", content: message.content }]
|
|
137
|
+
}
|
|
138
|
+
const out: OpenAIMessage[] = []
|
|
139
|
+
const toolResults = message.content.filter(
|
|
140
|
+
(b): b is ToolResultBlock => b.type === "tool_result",
|
|
141
|
+
)
|
|
142
|
+
const rest = message.content.filter((b) => b.type !== "tool_result")
|
|
143
|
+
|
|
144
|
+
// Protocol order: tool_use -> tool_result -> user.
|
|
145
|
+
const images: OpenAIImagePart[] = []
|
|
146
|
+
for (const result of toolResults) {
|
|
147
|
+
const { text, images: resultImages } = toolResultText(result)
|
|
148
|
+
images.push(...resultImages)
|
|
149
|
+
out.push({ role: "tool", tool_call_id: result.tool_use_id, content: text })
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const restTexts: string[] = []
|
|
153
|
+
const restImages: OpenAIImagePart[] = []
|
|
154
|
+
for (const view of classifyContent(rest)) {
|
|
155
|
+
if (view.kind === "text") restTexts.push(view.text)
|
|
156
|
+
else if (view.kind === "image") restImages.push(toImagePart(view.block))
|
|
157
|
+
// Never silence. A PDF attachment used to translate to nothing at all, so
|
|
158
|
+
// the model answered about a document it had never seen.
|
|
159
|
+
else if (view.kind === "unsupported") {
|
|
160
|
+
restTexts.push(view.text)
|
|
161
|
+
debugWarn(`dropped a ${view.blockType} block from a user message`)
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (images.length > 0 || restImages.length > 0) {
|
|
166
|
+
const parts: Array<OpenAITextPart | OpenAIImagePart> = [
|
|
167
|
+
...images,
|
|
168
|
+
...restImages,
|
|
169
|
+
]
|
|
170
|
+
const text = restTexts.join("\n\n")
|
|
171
|
+
if (text) parts.push({ type: "text", text })
|
|
172
|
+
out.push({ role: "user", content: parts })
|
|
173
|
+
} else if (rest.length > 0) {
|
|
174
|
+
out.push({ role: "user", content: restTexts.filter(Boolean).join("\n\n") })
|
|
175
|
+
}
|
|
176
|
+
return out
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function translateAssistantMessage(message: AnthropicMessage): OpenAIMessage[] {
|
|
180
|
+
if (typeof message.content === "string") {
|
|
181
|
+
return [{ role: "assistant", content: message.content }]
|
|
182
|
+
}
|
|
183
|
+
const toolUses = message.content.filter(
|
|
184
|
+
(b): b is ToolUseBlock => b.type === "tool_use",
|
|
185
|
+
)
|
|
186
|
+
// OpenAI has no thinking blocks; the classifier folds them into text (they
|
|
187
|
+
// are usually empty here anyway because CLAUDE_CODE_DISABLE_THINKING is set).
|
|
188
|
+
const text = classifyContent(message.content)
|
|
189
|
+
.map((view) => {
|
|
190
|
+
if (view.kind === "text") return view.text
|
|
191
|
+
if (view.kind === "unsupported") {
|
|
192
|
+
debugWarn(`dropped a ${view.blockType} block from an assistant message`)
|
|
193
|
+
return view.text
|
|
194
|
+
}
|
|
195
|
+
return ""
|
|
196
|
+
})
|
|
197
|
+
.filter(Boolean)
|
|
198
|
+
.join("\n\n")
|
|
199
|
+
|
|
200
|
+
if (toolUses.length > 0) {
|
|
201
|
+
return [
|
|
202
|
+
{
|
|
203
|
+
role: "assistant",
|
|
204
|
+
content: text || null,
|
|
205
|
+
tool_calls: toolUses.map((use) => ({
|
|
206
|
+
id: use.id,
|
|
207
|
+
type: "function" as const,
|
|
208
|
+
function: {
|
|
209
|
+
name: use.name,
|
|
210
|
+
arguments: JSON.stringify(use.input),
|
|
211
|
+
},
|
|
212
|
+
})),
|
|
213
|
+
},
|
|
214
|
+
]
|
|
215
|
+
}
|
|
216
|
+
return [{ role: "assistant", content: text }]
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function translateSystem(
|
|
220
|
+
system: AnthropicRequest["system"],
|
|
221
|
+
): OpenAIMessage[] {
|
|
222
|
+
if (!system) return []
|
|
223
|
+
const text =
|
|
224
|
+
typeof system === "string"
|
|
225
|
+
? system
|
|
226
|
+
: system.map((b) => b.text).join("\n\n")
|
|
227
|
+
return [
|
|
228
|
+
{
|
|
229
|
+
role: "system",
|
|
230
|
+
content: text,
|
|
231
|
+
...(marked(system) && { copilot_cache_control: COPILOT_CACHE }),
|
|
232
|
+
},
|
|
233
|
+
]
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function translateTools(
|
|
237
|
+
tools: AnthropicRequest["tools"],
|
|
238
|
+
): OpenAIRequest["tools"] {
|
|
239
|
+
if (!tools || tools.length === 0) return undefined
|
|
240
|
+
return tools.map((tool) => ({
|
|
241
|
+
type: "function" as const,
|
|
242
|
+
function: {
|
|
243
|
+
name: tool.name,
|
|
244
|
+
description: tool.description,
|
|
245
|
+
parameters: tool.input_schema,
|
|
246
|
+
},
|
|
247
|
+
}))
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function translateToolChoice(
|
|
251
|
+
choice: AnthropicRequest["tool_choice"],
|
|
252
|
+
): OpenAIRequest["tool_choice"] {
|
|
253
|
+
if (!choice) return undefined
|
|
254
|
+
switch (choice.type) {
|
|
255
|
+
case "auto":
|
|
256
|
+
return "auto"
|
|
257
|
+
case "any":
|
|
258
|
+
return "required"
|
|
259
|
+
case "none":
|
|
260
|
+
return "none"
|
|
261
|
+
case "tool":
|
|
262
|
+
return choice.name
|
|
263
|
+
? { type: "function", function: { name: choice.name } }
|
|
264
|
+
: undefined
|
|
265
|
+
default:
|
|
266
|
+
return undefined
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export function translateRequest(
|
|
271
|
+
payload: AnthropicRequest,
|
|
272
|
+
allowedEfforts?: string[] | null,
|
|
273
|
+
): OpenAIRequest {
|
|
274
|
+
const effort = effortFor(payload, allowedEfforts)
|
|
275
|
+
return {
|
|
276
|
+
model: normalizeModel(payload.model),
|
|
277
|
+
...(effort && { reasoning_effort: effort }),
|
|
278
|
+
messages: [
|
|
279
|
+
...translateSystem(payload.system),
|
|
280
|
+
...payload.messages.flatMap((message) => {
|
|
281
|
+
const out =
|
|
282
|
+
message.role === "user"
|
|
283
|
+
? translateUserMessage(message)
|
|
284
|
+
: translateAssistantMessage(message)
|
|
285
|
+
// Carry a cache breakpoint onto the last message this turn produced.
|
|
286
|
+
const last = out[out.length - 1]
|
|
287
|
+
if (last && marked(message.content)) {
|
|
288
|
+
last.copilot_cache_control = COPILOT_CACHE
|
|
289
|
+
}
|
|
290
|
+
return out
|
|
291
|
+
}),
|
|
292
|
+
],
|
|
293
|
+
max_tokens: payload.max_tokens,
|
|
294
|
+
stop: payload.stop_sequences?.length ? payload.stop_sequences : null,
|
|
295
|
+
stream: payload.stream,
|
|
296
|
+
stream_options: payload.stream ? { include_usage: true } : undefined,
|
|
297
|
+
temperature: payload.temperature,
|
|
298
|
+
top_p: payload.top_p,
|
|
299
|
+
user: payload.metadata?.user_id ?? null,
|
|
300
|
+
tools: translateTools(payload.tools),
|
|
301
|
+
tool_choice: translateToolChoice(payload.tool_choice),
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// ---------------------------------------------------------------------------
|
|
306
|
+
// Response direction: OpenAI -> Anthropic (non-streaming)
|
|
307
|
+
// ---------------------------------------------------------------------------
|
|
308
|
+
|
|
309
|
+
export function mapStopReason(
|
|
310
|
+
finish: OpenAIChoice["finish_reason"],
|
|
311
|
+
): AnthropicResponse["stop_reason"] {
|
|
312
|
+
if (finish === null) return null
|
|
313
|
+
switch (finish) {
|
|
314
|
+
case "stop":
|
|
315
|
+
return "end_turn"
|
|
316
|
+
case "length":
|
|
317
|
+
return "max_tokens"
|
|
318
|
+
case "tool_calls":
|
|
319
|
+
return "tool_use"
|
|
320
|
+
case "content_filter":
|
|
321
|
+
return "end_turn"
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
export function usageFromOpenAI(usage: OpenAIUsage | undefined) {
|
|
326
|
+
const cached = usage?.prompt_tokens_details?.cached_tokens
|
|
327
|
+
return {
|
|
328
|
+
input_tokens: Math.max(
|
|
329
|
+
0,
|
|
330
|
+
(usage?.prompt_tokens ?? 0) - (cached ?? 0),
|
|
331
|
+
),
|
|
332
|
+
output_tokens: usage?.completion_tokens ?? 0,
|
|
333
|
+
...(cached !== undefined && { cache_read_input_tokens: cached }),
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
export function translateResponse(upstream: OpenAIResponse): AnthropicResponse {
|
|
338
|
+
const choice = upstream.choices?.[0]
|
|
339
|
+
const content: AnthropicContentBlock[] = []
|
|
340
|
+
|
|
341
|
+
const messageContent = choice?.message?.content
|
|
342
|
+
if (typeof messageContent === "string" && messageContent.length > 0) {
|
|
343
|
+
content.push({ type: "text", text: messageContent })
|
|
344
|
+
} else if (Array.isArray(messageContent)) {
|
|
345
|
+
for (const part of messageContent) {
|
|
346
|
+
if (part.type === "text") content.push({ type: "text", text: part.text })
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
for (const call of choice?.message?.tool_calls ?? []) {
|
|
350
|
+
let input: Record<string, unknown> = {}
|
|
351
|
+
try {
|
|
352
|
+
input = JSON.parse(call.function.arguments || "{}") as Record<
|
|
353
|
+
string,
|
|
354
|
+
unknown
|
|
355
|
+
>
|
|
356
|
+
} catch {
|
|
357
|
+
// Malformed arguments: fall back to empty input rather than failing.
|
|
358
|
+
}
|
|
359
|
+
content.push({
|
|
360
|
+
type: "tool_use",
|
|
361
|
+
id: call.id,
|
|
362
|
+
name: call.function.name,
|
|
363
|
+
input,
|
|
364
|
+
})
|
|
365
|
+
}
|
|
366
|
+
if (content.length === 0) content.push({ type: "text", text: "" })
|
|
367
|
+
|
|
368
|
+
let stopReason = mapStopReason(choice?.finish_reason ?? null)
|
|
369
|
+
// "tool_use" with no tool_use block is not a valid Anthropic response.
|
|
370
|
+
if (stopReason === "tool_use" && !content.some((b) => b.type === "tool_use")) {
|
|
371
|
+
stopReason = "end_turn"
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
return {
|
|
375
|
+
id: upstream.id,
|
|
376
|
+
type: "message",
|
|
377
|
+
role: "assistant",
|
|
378
|
+
model: upstream.model,
|
|
379
|
+
content,
|
|
380
|
+
stop_reason: stopReason,
|
|
381
|
+
stop_sequence: null,
|
|
382
|
+
usage: usageFromOpenAI(upstream.usage),
|
|
383
|
+
}
|
|
384
|
+
}
|
package/src/wire.ts
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
// Wire shapes shared by the request and response dialects.
|
|
2
|
+
import type { ContentBlock, ToolUseBlock } from "./blocks"
|
|
3
|
+
|
|
4
|
+
export interface CacheControl {
|
|
5
|
+
cache_control?: { type?: string } | null
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface AnthropicTextBlock extends CacheControl {
|
|
9
|
+
type: "text"
|
|
10
|
+
text: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface AnthropicMessage {
|
|
14
|
+
role: "user" | "assistant"
|
|
15
|
+
content: string | ContentBlock[]
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface AnthropicTool {
|
|
19
|
+
name: string
|
|
20
|
+
description?: string
|
|
21
|
+
input_schema: Record<string, unknown>
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface AnthropicRequest {
|
|
25
|
+
model: string
|
|
26
|
+
max_tokens: number
|
|
27
|
+
messages: AnthropicMessage[]
|
|
28
|
+
system?: string | Array<AnthropicTextBlock>
|
|
29
|
+
tools?: AnthropicTool[]
|
|
30
|
+
tool_choice?: { type: "auto" | "any" | "tool" | "none"; name?: string }
|
|
31
|
+
stream?: boolean
|
|
32
|
+
temperature?: number
|
|
33
|
+
top_p?: number
|
|
34
|
+
stop_sequences?: string[]
|
|
35
|
+
metadata?: { user_id?: string }
|
|
36
|
+
thinking?: unknown
|
|
37
|
+
/** Claude Code's /effort setting rides here (gateway protocol). */
|
|
38
|
+
output_config?: { effort?: string }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface OpenAITextPart {
|
|
42
|
+
type: "text"
|
|
43
|
+
text: string
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface OpenAIImagePart {
|
|
47
|
+
type: "image_url"
|
|
48
|
+
image_url: { url: string }
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export type OpenAIContent = string | Array<OpenAITextPart | OpenAIImagePart> | null
|
|
52
|
+
|
|
53
|
+
export interface OpenAIToolCall {
|
|
54
|
+
id: string
|
|
55
|
+
type: "function"
|
|
56
|
+
function: { name: string; arguments: string }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface OpenAIMessage {
|
|
60
|
+
role: "system" | "user" | "assistant" | "tool"
|
|
61
|
+
content: OpenAIContent
|
|
62
|
+
tool_calls?: OpenAIToolCall[]
|
|
63
|
+
tool_call_id?: string
|
|
64
|
+
copilot_cache_control?: { type: "ephemeral" }
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface OpenAIRequest {
|
|
68
|
+
model: string
|
|
69
|
+
messages: OpenAIMessage[]
|
|
70
|
+
max_tokens?: number
|
|
71
|
+
stop?: string[] | null
|
|
72
|
+
stream?: boolean
|
|
73
|
+
stream_options?: { include_usage: boolean }
|
|
74
|
+
reasoning_effort?: string
|
|
75
|
+
temperature?: number
|
|
76
|
+
top_p?: number
|
|
77
|
+
user?: string | null
|
|
78
|
+
tools?: Array<{
|
|
79
|
+
type: "function"
|
|
80
|
+
function: {
|
|
81
|
+
name: string
|
|
82
|
+
description?: string
|
|
83
|
+
parameters: Record<string, unknown>
|
|
84
|
+
}
|
|
85
|
+
}> | null
|
|
86
|
+
tool_choice?:
|
|
87
|
+
| "none"
|
|
88
|
+
| "auto"
|
|
89
|
+
| "required"
|
|
90
|
+
| { type: "function"; function: { name: string } }
|
|
91
|
+
| null
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export interface OpenAIUsage {
|
|
95
|
+
prompt_tokens?: number
|
|
96
|
+
completion_tokens?: number
|
|
97
|
+
prompt_tokens_details?: { cached_tokens?: number }
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export interface OpenAIChoice {
|
|
101
|
+
index: number
|
|
102
|
+
finish_reason: "stop" | "length" | "tool_calls" | "content_filter" | null
|
|
103
|
+
message?: { role: "assistant"; content: OpenAIContent; tool_calls?: OpenAIToolCall[] }
|
|
104
|
+
delta?: {
|
|
105
|
+
role?: string
|
|
106
|
+
content?: string | null
|
|
107
|
+
tool_calls?: Array<{
|
|
108
|
+
index: number
|
|
109
|
+
id?: string
|
|
110
|
+
function?: { name?: string; arguments?: string }
|
|
111
|
+
}>
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export interface OpenAIResponse {
|
|
116
|
+
id: string
|
|
117
|
+
model: string
|
|
118
|
+
choices?: OpenAIChoice[]
|
|
119
|
+
usage?: OpenAIUsage
|
|
120
|
+
error?: { message?: string; code?: string | number }
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ---------------------------------------------------------------------------
|
|
124
|
+
// Anthropic response types
|
|
125
|
+
// ---------------------------------------------------------------------------
|
|
126
|
+
|
|
127
|
+
export type AnthropicContentBlock =
|
|
128
|
+
| AnthropicTextBlock
|
|
129
|
+
| ToolUseBlock
|
|
130
|
+
|
|
131
|
+
export interface AnthropicResponse {
|
|
132
|
+
id: string
|
|
133
|
+
type: "message"
|
|
134
|
+
role: "assistant"
|
|
135
|
+
model: string
|
|
136
|
+
content: AnthropicContentBlock[]
|
|
137
|
+
stop_reason: "end_turn" | "max_tokens" | "tool_use" | null
|
|
138
|
+
stop_sequence: null
|
|
139
|
+
usage: {
|
|
140
|
+
input_tokens: number
|
|
141
|
+
output_tokens: number
|
|
142
|
+
cache_read_input_tokens?: number
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export interface StreamEventData {
|
|
147
|
+
event: string
|
|
148
|
+
data: Record<string, unknown>
|
|
149
|
+
}
|
package/uninstall.sh
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# clco uninstaller.
|
|
3
|
+
# curl -fsSL https://raw.githubusercontent.com/semanticist21/clco/main/uninstall.sh | bash
|
|
4
|
+
# ... --full : also remove config and the GitHub token (~/.config/clco)
|
|
5
|
+
set -euo pipefail
|
|
6
|
+
|
|
7
|
+
CLCO_DIR="${CLCO_DIR:-$HOME/.local/share/clco}"
|
|
8
|
+
BIN_DIR="${BIN_DIR:-$HOME/.local/bin}"
|
|
9
|
+
|
|
10
|
+
log() { printf '\033[1;36m==>\033[0m %s\n' "$*"; }
|
|
11
|
+
fail() { printf '\033[1;31mError:\033[0m %s\n' "$*" >&2; exit 1; }
|
|
12
|
+
|
|
13
|
+
removed=0
|
|
14
|
+
if [ -f "$BIN_DIR/clco" ]; then rm -f "$BIN_DIR/clco"; log "Removed: $BIN_DIR/clco"; removed=1; fi
|
|
15
|
+
if [ -d "$CLCO_DIR" ]; then
|
|
16
|
+
if [ ! -d "$CLCO_DIR/.git" ]; then
|
|
17
|
+
fail "$CLCO_DIR is not a clco install (git repo) - check it yourself before deleting"
|
|
18
|
+
fi
|
|
19
|
+
rm -rf "$CLCO_DIR"; log "Removed: $CLCO_DIR"; removed=1
|
|
20
|
+
fi
|
|
21
|
+
if [ -d "$CLCO_DIR.previous" ]; then
|
|
22
|
+
[ -d "$CLCO_DIR.previous/.git" ] \
|
|
23
|
+
|| fail "$CLCO_DIR.previous is not a clco rollback install - check it yourself before deleting"
|
|
24
|
+
rm -rf "$CLCO_DIR.previous"; log "Removed: $CLCO_DIR.previous"; removed=1
|
|
25
|
+
fi
|
|
26
|
+
if [ -d "$CLCO_DIR.update.lock" ]; then
|
|
27
|
+
rmdir "$CLCO_DIR.update.lock" \
|
|
28
|
+
|| fail "$CLCO_DIR.update.lock is not empty - verify no update is running before removing it"
|
|
29
|
+
log "Removed stale update lock: $CLCO_DIR.update.lock"; removed=1
|
|
30
|
+
fi
|
|
31
|
+
|
|
32
|
+
if [ "${1:-}" = "--full" ]; then
|
|
33
|
+
if [ -d "$HOME/.config/clco" ]; then
|
|
34
|
+
rm -rf "$HOME/.config/clco"
|
|
35
|
+
log "Removed: ~/.config/clco (including the GitHub token)"
|
|
36
|
+
fi
|
|
37
|
+
else
|
|
38
|
+
[ -d "$HOME/.config/clco" ] && log "Kept config: ~/.config/clco (including the GitHub token) - remove it with: $0 --full"
|
|
39
|
+
fi
|
|
40
|
+
|
|
41
|
+
[ "$removed" = "1" ] || [ "${1:-}" = "--full" ] || log "Nothing to remove"
|
|
42
|
+
log "Done"
|