pi-commandcode-provider 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 +110 -0
- package/index.ts +197 -0
- package/package.json +54 -0
- package/src/auth-server.ts +214 -0
- package/src/converters.ts +276 -0
- package/src/core.ts +458 -0
- package/src/oauth.ts +167 -0
- package/src/types.ts +156 -0
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs"
|
|
2
|
+
import { homedir } from "node:os"
|
|
3
|
+
import { join } from "node:path"
|
|
4
|
+
|
|
5
|
+
import type { MessageLike, StopReason, ToolLike } from "./types.ts"
|
|
6
|
+
|
|
7
|
+
export function isRecord(value: unknown): value is Record<string, unknown> {
|
|
8
|
+
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function stringValue(value: unknown): string | undefined {
|
|
12
|
+
return typeof value === "string" ? value : undefined
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function booleanValue(value: unknown): boolean | undefined {
|
|
16
|
+
return typeof value === "boolean" ? value : undefined
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function recordArray(value: unknown): readonly Record<string, unknown>[] {
|
|
20
|
+
if (!Array.isArray(value)) return []
|
|
21
|
+
return value.filter(isRecord)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function recordOrEmpty(value: unknown): Record<string, unknown> {
|
|
25
|
+
if (isRecord(value)) return value
|
|
26
|
+
if (typeof value === "string") {
|
|
27
|
+
try {
|
|
28
|
+
const parsed: unknown = JSON.parse(value)
|
|
29
|
+
if (isRecord(parsed)) return parsed
|
|
30
|
+
} catch {
|
|
31
|
+
// Some providers stream incomplete JSON argument fragments.
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return {}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function numberValue(value: unknown): number | undefined {
|
|
38
|
+
return typeof value === "number" && Number.isFinite(value) ? value : undefined
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function defaultAuthPaths(home: string): string[] {
|
|
42
|
+
return [join(home, ".commandcode", "auth.json"), join(home, ".pi", "agent", "auth.json")]
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function getApiKey(
|
|
46
|
+
options: {
|
|
47
|
+
env?: NodeJS.ProcessEnv
|
|
48
|
+
authPaths?: readonly string[]
|
|
49
|
+
homeDir?: () => string
|
|
50
|
+
} = {},
|
|
51
|
+
): string | undefined {
|
|
52
|
+
const env = options.env ?? process.env
|
|
53
|
+
if (env.COMMANDCODE_API_KEY) return env.COMMANDCODE_API_KEY
|
|
54
|
+
|
|
55
|
+
const home = options.homeDir?.() ?? homedir()
|
|
56
|
+
const authPaths = options.authPaths ?? defaultAuthPaths(home)
|
|
57
|
+
|
|
58
|
+
for (const authPath of authPaths) {
|
|
59
|
+
try {
|
|
60
|
+
if (!existsSync(authPath)) continue
|
|
61
|
+
const parsed: unknown = JSON.parse(readFileSync(authPath, "utf-8"))
|
|
62
|
+
if (!isRecord(parsed)) continue
|
|
63
|
+
|
|
64
|
+
// Legacy: direct apiKey or commandcode field
|
|
65
|
+
const apiKey = stringValue(parsed.apiKey)
|
|
66
|
+
if (apiKey) return apiKey
|
|
67
|
+
const commandcode = stringValue(parsed.commandcode)
|
|
68
|
+
if (commandcode) return commandcode
|
|
69
|
+
|
|
70
|
+
// OAuth: pi stores OAuth credentials as {"commandcode": {"type":"oauth","access":"...","refresh":"...","expires":...}}
|
|
71
|
+
const providerKey = isRecord(parsed.commandcode) ? parsed.commandcode : undefined
|
|
72
|
+
if (providerKey && stringValue(providerKey.type) === "oauth") {
|
|
73
|
+
const access = stringValue(providerKey.access)
|
|
74
|
+
if (access) return access
|
|
75
|
+
}
|
|
76
|
+
} catch {
|
|
77
|
+
// Ignore malformed or unreadable auth files.
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return undefined
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function textContent(message: { content?: unknown }): string {
|
|
85
|
+
return recordArray(message.content)
|
|
86
|
+
.filter((part) => part.type === "text")
|
|
87
|
+
.map((part) => stringValue(part.text) ?? "")
|
|
88
|
+
.join("\n")
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function getEnvironmentInfo(): string {
|
|
92
|
+
return `${process.platform}-${process.arch}, Node.js ${process.version}`
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function toJsonSchema(schema: unknown): unknown {
|
|
96
|
+
if (!isRecord(schema)) return {}
|
|
97
|
+
|
|
98
|
+
const kind = stringValue(schema.kind) ?? stringValue(schema.type)
|
|
99
|
+
const enumValues = Array.isArray(schema.enum) ? schema.enum : undefined
|
|
100
|
+
if (enumValues) {
|
|
101
|
+
return { type: typeof enumValues[0], enum: enumValues }
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
switch (kind) {
|
|
105
|
+
case "string":
|
|
106
|
+
case "String":
|
|
107
|
+
return { type: "string" }
|
|
108
|
+
case "number":
|
|
109
|
+
case "Number":
|
|
110
|
+
return { type: "number" }
|
|
111
|
+
case "boolean":
|
|
112
|
+
case "Boolean":
|
|
113
|
+
return { type: "boolean" }
|
|
114
|
+
case "object":
|
|
115
|
+
case "Object": {
|
|
116
|
+
const properties: Record<string, unknown> = {}
|
|
117
|
+
const inferredRequired: string[] = []
|
|
118
|
+
const sourceProperties = isRecord(schema.properties) ? schema.properties : undefined
|
|
119
|
+
const optional = Array.isArray(schema.optional)
|
|
120
|
+
? schema.optional.filter((item): item is string => typeof item === "string")
|
|
121
|
+
: []
|
|
122
|
+
|
|
123
|
+
if (sourceProperties) {
|
|
124
|
+
for (const [key, value] of Object.entries(sourceProperties)) {
|
|
125
|
+
properties[key] = toJsonSchema(value)
|
|
126
|
+
const valueRecord = isRecord(value) ? value : undefined
|
|
127
|
+
if (booleanValue(valueRecord?.optional) !== true && !optional.includes(key)) {
|
|
128
|
+
inferredRequired.push(key)
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const explicitRequired = Array.isArray(schema.required)
|
|
134
|
+
? schema.required.filter((item): item is string => typeof item === "string")
|
|
135
|
+
: undefined
|
|
136
|
+
const required = explicitRequired ?? inferredRequired
|
|
137
|
+
const out: Record<string, unknown> = { type: "object" }
|
|
138
|
+
if (Object.keys(properties).length > 0) out.properties = properties
|
|
139
|
+
if (required.length > 0) out.required = required
|
|
140
|
+
return out
|
|
141
|
+
}
|
|
142
|
+
case "array":
|
|
143
|
+
case "Array":
|
|
144
|
+
return {
|
|
145
|
+
type: "array",
|
|
146
|
+
items: toJsonSchema(schema.items ?? schema.element),
|
|
147
|
+
}
|
|
148
|
+
case "union":
|
|
149
|
+
case "Union": {
|
|
150
|
+
const variants = Array.isArray(schema.variants)
|
|
151
|
+
? schema.variants
|
|
152
|
+
: Array.isArray(schema.anyOf)
|
|
153
|
+
? schema.anyOf
|
|
154
|
+
: []
|
|
155
|
+
for (const variant of variants) {
|
|
156
|
+
const converted = toJsonSchema(variant)
|
|
157
|
+
if (isRecord(converted) && Object.keys(converted).length > 0) return converted
|
|
158
|
+
}
|
|
159
|
+
return {}
|
|
160
|
+
}
|
|
161
|
+
case "optional":
|
|
162
|
+
case "Optional":
|
|
163
|
+
return toJsonSchema(schema.wrapped ?? schema.inner)
|
|
164
|
+
default:
|
|
165
|
+
return {}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function toolsToJson(tools?: readonly ToolLike[]): unknown[] {
|
|
170
|
+
if (!tools) return []
|
|
171
|
+
return tools.map((tool) => ({
|
|
172
|
+
type: "function",
|
|
173
|
+
name: tool.name,
|
|
174
|
+
description: tool.description,
|
|
175
|
+
input_schema: tool.parameters ? toJsonSchema(tool.parameters) : {},
|
|
176
|
+
}))
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function completeToolCallIds(messages?: readonly MessageLike[]): Set<string> {
|
|
180
|
+
const callIds = new Set<string>()
|
|
181
|
+
const resultIds = new Set<string>()
|
|
182
|
+
|
|
183
|
+
for (const message of messages ?? []) {
|
|
184
|
+
if (message.role === "assistant") {
|
|
185
|
+
for (const content of recordArray(message.content)) {
|
|
186
|
+
if (content.type === "toolCall") {
|
|
187
|
+
const id = stringValue(content.id)
|
|
188
|
+
if (id) callIds.add(id)
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
} else if (message.role === "toolResult") {
|
|
192
|
+
if (message.toolCallId) resultIds.add(message.toolCallId)
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return new Set([...callIds].filter((id) => resultIds.has(id)))
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export function messagesToCC(messages?: readonly MessageLike[]): unknown[] {
|
|
200
|
+
const out: unknown[] = []
|
|
201
|
+
const pairedToolCallIds = completeToolCallIds(messages)
|
|
202
|
+
|
|
203
|
+
for (const message of messages ?? []) {
|
|
204
|
+
if (message.role === "user") {
|
|
205
|
+
out.push({
|
|
206
|
+
role: "user",
|
|
207
|
+
content: typeof message.content === "string" ? message.content : message.content,
|
|
208
|
+
})
|
|
209
|
+
} else if (message.role === "assistant") {
|
|
210
|
+
const parts: unknown[] = []
|
|
211
|
+
for (const content of recordArray(message.content)) {
|
|
212
|
+
if (content.type === "text") {
|
|
213
|
+
parts.push({ type: "text", text: stringValue(content.text) ?? "" })
|
|
214
|
+
} else if (content.type === "thinking") {
|
|
215
|
+
parts.push({
|
|
216
|
+
type: "reasoning",
|
|
217
|
+
text: stringValue(content.thinking) ?? "",
|
|
218
|
+
})
|
|
219
|
+
} else if (content.type === "toolCall") {
|
|
220
|
+
const toolCallId = stringValue(content.id) ?? ""
|
|
221
|
+
if (!pairedToolCallIds.has(toolCallId)) continue
|
|
222
|
+
parts.push({
|
|
223
|
+
type: "tool-call",
|
|
224
|
+
toolCallId,
|
|
225
|
+
toolName: stringValue(content.name) ?? "",
|
|
226
|
+
input: recordOrEmpty(content.arguments),
|
|
227
|
+
})
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
if (parts.length > 0) out.push({ role: "assistant", content: parts })
|
|
231
|
+
} else if (message.role === "toolResult") {
|
|
232
|
+
if (!message.toolCallId || !pairedToolCallIds.has(message.toolCallId)) continue
|
|
233
|
+
out.push({
|
|
234
|
+
role: "tool",
|
|
235
|
+
content: [
|
|
236
|
+
{
|
|
237
|
+
type: "tool-result",
|
|
238
|
+
toolCallId: message.toolCallId,
|
|
239
|
+
toolName: message.toolName,
|
|
240
|
+
output: message.isError
|
|
241
|
+
? { type: "error-text", value: textContent(message) }
|
|
242
|
+
: { type: "text", value: textContent(message) },
|
|
243
|
+
},
|
|
244
|
+
],
|
|
245
|
+
})
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
return out
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export function parseStreamEventLine(line: string): unknown | undefined {
|
|
252
|
+
let trimmed = line.trim()
|
|
253
|
+
if (!trimmed || trimmed.startsWith(":") || trimmed.startsWith("event:")) return undefined
|
|
254
|
+
if (trimmed.startsWith("data:")) trimmed = trimmed.slice(5).trim()
|
|
255
|
+
if (!trimmed || trimmed === "[DONE]") return undefined
|
|
256
|
+
|
|
257
|
+
try {
|
|
258
|
+
const parsed: unknown = JSON.parse(trimmed)
|
|
259
|
+
return parsed
|
|
260
|
+
} catch {
|
|
261
|
+
return undefined
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
export function mapFinishReason(reason: unknown): StopReason {
|
|
266
|
+
if (reason === "tool-calls") return "toolUse"
|
|
267
|
+
if (
|
|
268
|
+
reason === "length" ||
|
|
269
|
+
reason === "max_tokens" ||
|
|
270
|
+
reason === "max-tokens" ||
|
|
271
|
+
reason === "max_output_tokens"
|
|
272
|
+
) {
|
|
273
|
+
return "length"
|
|
274
|
+
}
|
|
275
|
+
return "stop"
|
|
276
|
+
}
|