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.
@@ -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
+ }