style: configure prettier (no semicolons, trailing commas)
This commit is contained in:
+68
-74
@@ -5,21 +5,21 @@
|
||||
* website POSTs the user's API key to /callback after they authenticate.
|
||||
*/
|
||||
|
||||
import { createServer, type Server } from "node:http";
|
||||
import type { AddressInfo } from "node:net";
|
||||
import { createServer, type Server } from "node:http"
|
||||
import type { AddressInfo } from "node:net"
|
||||
|
||||
export interface AuthCallback {
|
||||
apiKey: string;
|
||||
state: string;
|
||||
userId: string;
|
||||
userName: string;
|
||||
keyName: string;
|
||||
apiKey: string
|
||||
state: string
|
||||
userId: string
|
||||
userName: string
|
||||
keyName: string
|
||||
}
|
||||
|
||||
export interface AuthServer {
|
||||
server: Server;
|
||||
port: number;
|
||||
waitForCallback: Promise<AuthCallback>;
|
||||
server: Server
|
||||
port: number
|
||||
waitForCallback: Promise<AuthCallback>
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -29,127 +29,121 @@ export interface AuthServer {
|
||||
* The server accepts exactly one valid POST to /callback and then closes.
|
||||
*/
|
||||
export function startAuthServer(): Promise<AuthServer> {
|
||||
let resolveCallback: (value: AuthCallback) => void;
|
||||
let rejectCallback: (error: Error) => void;
|
||||
let resolveCallback: (value: AuthCallback) => void
|
||||
let rejectCallback: (error: Error) => void
|
||||
|
||||
const waitForCallback = new Promise<AuthCallback>((resolve, reject) => {
|
||||
resolveCallback = resolve;
|
||||
rejectCallback = reject;
|
||||
});
|
||||
resolveCallback = resolve
|
||||
rejectCallback = reject
|
||||
})
|
||||
|
||||
const server = createServer((req, res) => {
|
||||
// CORS: allow requests from Command Code domains and localhost for dev
|
||||
const origin = req.headers.origin || "";
|
||||
const origin = req.headers.origin || ""
|
||||
const allowedOrigins = [
|
||||
"http://localhost:3000",
|
||||
"https://staging.commandcode.ai",
|
||||
"https://commandcode.ai",
|
||||
];
|
||||
const responseOrigin = allowedOrigins.includes(origin)
|
||||
? origin
|
||||
: allowedOrigins[0];
|
||||
]
|
||||
const responseOrigin = allowedOrigins.includes(origin) ? origin : allowedOrigins[0]
|
||||
|
||||
res.setHeader("Access-Control-Allow-Origin", responseOrigin);
|
||||
res.setHeader("Access-Control-Allow-Methods", "POST, OPTIONS");
|
||||
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
||||
res.setHeader("Content-Type", "application/json");
|
||||
res.setHeader("Access-Control-Allow-Origin", responseOrigin)
|
||||
res.setHeader("Access-Control-Allow-Methods", "POST, OPTIONS")
|
||||
res.setHeader("Access-Control-Allow-Headers", "Content-Type")
|
||||
res.setHeader("Content-Type", "application/json")
|
||||
|
||||
// Handle CORS preflight
|
||||
if (req.method === "OPTIONS") {
|
||||
res.writeHead(204);
|
||||
res.end();
|
||||
return;
|
||||
res.writeHead(204)
|
||||
res.end()
|
||||
return
|
||||
}
|
||||
|
||||
if (req.url !== "/callback") {
|
||||
res.writeHead(404);
|
||||
res.end(JSON.stringify({ success: false, error: "Not found" }));
|
||||
return;
|
||||
res.writeHead(404)
|
||||
res.end(JSON.stringify({ success: false, error: "Not found" }))
|
||||
return
|
||||
}
|
||||
|
||||
if (req.method !== "POST") {
|
||||
res.writeHead(405);
|
||||
res.writeHead(405)
|
||||
res.end(
|
||||
JSON.stringify({
|
||||
success: false,
|
||||
error: "Method not allowed. Use POST.",
|
||||
}),
|
||||
);
|
||||
return;
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
let body = "";
|
||||
let body = ""
|
||||
req.on("data", (chunk) => {
|
||||
body += chunk.toString();
|
||||
if (body.length > 10_000) req.destroy();
|
||||
});
|
||||
body += chunk.toString()
|
||||
if (body.length > 10_000) req.destroy()
|
||||
})
|
||||
|
||||
req.on("end", () => {
|
||||
try {
|
||||
const parsed = JSON.parse(body) as Record<string, unknown>;
|
||||
const parsed = JSON.parse(body) as Record<string, unknown>
|
||||
|
||||
if (parsed.error) {
|
||||
res.writeHead(200);
|
||||
res.end(JSON.stringify({ success: true }));
|
||||
res.writeHead(200)
|
||||
res.end(JSON.stringify({ success: true }))
|
||||
const description =
|
||||
typeof parsed.error_description === "string"
|
||||
? parsed.error_description
|
||||
: String(parsed.error);
|
||||
: String(parsed.error)
|
||||
if (parsed.error === "access_denied") {
|
||||
rejectCallback(
|
||||
new Error(description || "Authorization was denied by the user"),
|
||||
);
|
||||
rejectCallback(new Error(description || "Authorization was denied by the user"))
|
||||
} else {
|
||||
rejectCallback(new Error(description || String(parsed.error)));
|
||||
rejectCallback(new Error(description || String(parsed.error)))
|
||||
}
|
||||
server.close();
|
||||
return;
|
||||
server.close()
|
||||
return
|
||||
}
|
||||
|
||||
const apiKey = typeof parsed.apiKey === "string" ? parsed.apiKey : "";
|
||||
const state = typeof parsed.state === "string" ? parsed.state : "";
|
||||
const userId = typeof parsed.userId === "string" ? parsed.userId : "";
|
||||
const userName =
|
||||
typeof parsed.userName === "string" ? parsed.userName : "";
|
||||
const keyName =
|
||||
typeof parsed.keyName === "string" ? parsed.keyName : "";
|
||||
const apiKey = typeof parsed.apiKey === "string" ? parsed.apiKey : ""
|
||||
const state = typeof parsed.state === "string" ? parsed.state : ""
|
||||
const userId = typeof parsed.userId === "string" ? parsed.userId : ""
|
||||
const userName = typeof parsed.userName === "string" ? parsed.userName : ""
|
||||
const keyName = typeof parsed.keyName === "string" ? parsed.keyName : ""
|
||||
|
||||
if (!apiKey || !state || !userId || !userName || !keyName) {
|
||||
res.writeHead(400);
|
||||
res.writeHead(400)
|
||||
res.end(
|
||||
JSON.stringify({
|
||||
success: false,
|
||||
error: "Missing required fields",
|
||||
}),
|
||||
);
|
||||
return;
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
res.writeHead(200);
|
||||
res.end(JSON.stringify({ success: true }));
|
||||
res.writeHead(200)
|
||||
res.end(JSON.stringify({ success: true }))
|
||||
|
||||
resolveCallback({ apiKey, state, userId, userName, keyName });
|
||||
server.close();
|
||||
resolveCallback({ apiKey, state, userId, userName, keyName })
|
||||
server.close()
|
||||
} catch {
|
||||
res.writeHead(400);
|
||||
res.end(JSON.stringify({ success: false, error: "Invalid JSON" }));
|
||||
res.writeHead(400)
|
||||
res.end(JSON.stringify({ success: false, error: "Invalid JSON" }))
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
req.on("error", () => {
|
||||
res.writeHead(500);
|
||||
res.end(JSON.stringify({ success: false, error: "Request error" }));
|
||||
});
|
||||
});
|
||||
res.writeHead(500)
|
||||
res.end(JSON.stringify({ success: false, error: "Request error" }))
|
||||
})
|
||||
})
|
||||
|
||||
return new Promise((resolve) => {
|
||||
server.on("error", (err: NodeJS.ErrnoException) => {
|
||||
rejectCallback(new Error(`Failed to start auth server: ${err.message}`));
|
||||
});
|
||||
rejectCallback(new Error(`Failed to start auth server: ${err.message}`))
|
||||
})
|
||||
|
||||
server.listen(0, "127.0.0.1", () => {
|
||||
const address = server.address() as AddressInfo;
|
||||
resolve({ server, port: address.port, waitForCallback });
|
||||
});
|
||||
});
|
||||
const address = server.address() as AddressInfo
|
||||
resolve({ server, port: address.port, waitForCallback })
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
+98
-122
@@ -1,258 +1,235 @@
|
||||
import { existsSync, readFileSync } from "node:fs";
|
||||
import { homedir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { existsSync, readFileSync } from "node:fs"
|
||||
import { homedir } from "node:os"
|
||||
import { join } from "node:path"
|
||||
|
||||
import type { MessageLike, StopReason, ToolLike } from "./types.ts";
|
||||
import type { MessageLike, StopReason, ToolLike } from "./types.ts"
|
||||
|
||||
export function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value)
|
||||
}
|
||||
|
||||
export function stringValue(value: unknown): string | undefined {
|
||||
return typeof value === "string" ? value : undefined;
|
||||
return typeof value === "string" ? value : undefined
|
||||
}
|
||||
|
||||
function booleanValue(value: unknown): boolean | undefined {
|
||||
return typeof value === "boolean" ? value : undefined;
|
||||
return typeof value === "boolean" ? value : undefined
|
||||
}
|
||||
|
||||
export function recordArray(
|
||||
value: unknown,
|
||||
): readonly Record<string, unknown>[] {
|
||||
if (!Array.isArray(value)) return [];
|
||||
return value.filter(isRecord);
|
||||
export function recordArray(value: unknown): readonly Record<string, unknown>[] {
|
||||
if (!Array.isArray(value)) return []
|
||||
return value.filter(isRecord)
|
||||
}
|
||||
|
||||
export function recordOrEmpty(value: unknown): Record<string, unknown> {
|
||||
if (isRecord(value)) return value;
|
||||
if (isRecord(value)) return value
|
||||
if (typeof value === "string") {
|
||||
try {
|
||||
const parsed: unknown = JSON.parse(value);
|
||||
if (isRecord(parsed)) return parsed;
|
||||
const parsed: unknown = JSON.parse(value)
|
||||
if (isRecord(parsed)) return parsed
|
||||
} catch {
|
||||
// Some providers stream incomplete JSON argument fragments.
|
||||
}
|
||||
}
|
||||
return {};
|
||||
return {}
|
||||
}
|
||||
|
||||
export function numberValue(value: unknown): number | undefined {
|
||||
return typeof value === "number" && Number.isFinite(value)
|
||||
? value
|
||||
: undefined;
|
||||
return typeof value === "number" && Number.isFinite(value) ? value : undefined
|
||||
}
|
||||
|
||||
function defaultAuthPaths(home: string): string[] {
|
||||
return [
|
||||
join(home, ".commandcode", "auth.json"),
|
||||
join(home, ".pi", "agent", "auth.json"),
|
||||
];
|
||||
return [join(home, ".commandcode", "auth.json"), join(home, ".pi", "agent", "auth.json")]
|
||||
}
|
||||
|
||||
export function getApiKey(
|
||||
options: {
|
||||
env?: NodeJS.ProcessEnv;
|
||||
authPaths?: readonly string[];
|
||||
homeDir?: () => string;
|
||||
env?: NodeJS.ProcessEnv
|
||||
authPaths?: readonly string[]
|
||||
homeDir?: () => string
|
||||
} = {},
|
||||
): string | undefined {
|
||||
const env = options.env ?? process.env;
|
||||
if (env.COMMANDCODE_API_KEY) return env.COMMANDCODE_API_KEY;
|
||||
const env = options.env ?? process.env
|
||||
if (env.COMMANDCODE_API_KEY) return env.COMMANDCODE_API_KEY
|
||||
|
||||
const home = options.homeDir?.() ?? homedir();
|
||||
const authPaths = options.authPaths ?? defaultAuthPaths(home);
|
||||
const home = options.homeDir?.() ?? homedir()
|
||||
const authPaths = options.authPaths ?? defaultAuthPaths(home)
|
||||
|
||||
for (const authPath of authPaths) {
|
||||
try {
|
||||
if (!existsSync(authPath)) continue;
|
||||
const parsed: unknown = JSON.parse(readFileSync(authPath, "utf-8"));
|
||||
if (!isRecord(parsed)) continue;
|
||||
if (!existsSync(authPath)) continue
|
||||
const parsed: unknown = JSON.parse(readFileSync(authPath, "utf-8"))
|
||||
if (!isRecord(parsed)) continue
|
||||
|
||||
// Legacy: direct apiKey or commandcode field
|
||||
const apiKey = stringValue(parsed.apiKey);
|
||||
if (apiKey) return apiKey;
|
||||
const commandcode = stringValue(parsed.commandcode);
|
||||
if (commandcode) return commandcode;
|
||||
const apiKey = stringValue(parsed.apiKey)
|
||||
if (apiKey) return apiKey
|
||||
const commandcode = stringValue(parsed.commandcode)
|
||||
if (commandcode) return commandcode
|
||||
|
||||
// OAuth: pi stores OAuth credentials as {"commandcode": {"type":"oauth","access":"...","refresh":"...","expires":...}}
|
||||
const providerKey = isRecord(parsed.commandcode)
|
||||
? parsed.commandcode
|
||||
: undefined;
|
||||
const providerKey = isRecord(parsed.commandcode) ? parsed.commandcode : undefined
|
||||
if (providerKey && stringValue(providerKey.type) === "oauth") {
|
||||
const access = stringValue(providerKey.access);
|
||||
if (access) return access;
|
||||
const access = stringValue(providerKey.access)
|
||||
if (access) return access
|
||||
}
|
||||
} catch {
|
||||
// Ignore malformed or unreadable auth files.
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
return undefined
|
||||
}
|
||||
|
||||
export function textContent(message: { content?: unknown }): string {
|
||||
return recordArray(message.content)
|
||||
.filter((part) => part.type === "text")
|
||||
.map((part) => stringValue(part.text) ?? "")
|
||||
.join("\n");
|
||||
.join("\n")
|
||||
}
|
||||
|
||||
export function getEnvironmentInfo(): string {
|
||||
return `${process.platform}-${process.arch}, Node.js ${process.version}`;
|
||||
return `${process.platform}-${process.arch}, Node.js ${process.version}`
|
||||
}
|
||||
|
||||
export function toJsonSchema(schema: unknown): unknown {
|
||||
if (!isRecord(schema)) return {};
|
||||
if (!isRecord(schema)) return {}
|
||||
|
||||
const kind = stringValue(schema.kind) ?? stringValue(schema.type);
|
||||
const enumValues = Array.isArray(schema.enum) ? schema.enum : undefined;
|
||||
const kind = stringValue(schema.kind) ?? stringValue(schema.type)
|
||||
const enumValues = Array.isArray(schema.enum) ? schema.enum : undefined
|
||||
if (enumValues) {
|
||||
return { type: typeof enumValues[0], enum: enumValues };
|
||||
return { type: typeof enumValues[0], enum: enumValues }
|
||||
}
|
||||
|
||||
switch (kind) {
|
||||
case "string":
|
||||
case "String":
|
||||
return { type: "string" };
|
||||
return { type: "string" }
|
||||
case "number":
|
||||
case "Number":
|
||||
return { type: "number" };
|
||||
return { type: "number" }
|
||||
case "boolean":
|
||||
case "Boolean":
|
||||
return { type: "boolean" };
|
||||
return { type: "boolean" }
|
||||
case "object":
|
||||
case "Object": {
|
||||
const properties: Record<string, unknown> = {};
|
||||
const inferredRequired: string[] = [];
|
||||
const sourceProperties = isRecord(schema.properties)
|
||||
? schema.properties
|
||||
: undefined;
|
||||
const properties: Record<string, unknown> = {}
|
||||
const inferredRequired: string[] = []
|
||||
const sourceProperties = isRecord(schema.properties) ? schema.properties : undefined
|
||||
const optional = Array.isArray(schema.optional)
|
||||
? schema.optional.filter(
|
||||
(item): item is string => typeof item === "string",
|
||||
)
|
||||
: [];
|
||||
? schema.optional.filter((item): item is string => typeof item === "string")
|
||||
: []
|
||||
|
||||
if (sourceProperties) {
|
||||
for (const [key, value] of Object.entries(sourceProperties)) {
|
||||
properties[key] = toJsonSchema(value);
|
||||
const valueRecord = isRecord(value) ? value : undefined;
|
||||
if (
|
||||
booleanValue(valueRecord?.optional) !== true &&
|
||||
!optional.includes(key)
|
||||
) {
|
||||
inferredRequired.push(key);
|
||||
properties[key] = toJsonSchema(value)
|
||||
const valueRecord = isRecord(value) ? value : undefined
|
||||
if (booleanValue(valueRecord?.optional) !== true && !optional.includes(key)) {
|
||||
inferredRequired.push(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const explicitRequired = Array.isArray(schema.required)
|
||||
? schema.required.filter(
|
||||
(item): item is string => typeof item === "string",
|
||||
)
|
||||
: undefined;
|
||||
const required = explicitRequired ?? inferredRequired;
|
||||
const out: Record<string, unknown> = { type: "object" };
|
||||
if (Object.keys(properties).length > 0) out.properties = properties;
|
||||
if (required.length > 0) out.required = required;
|
||||
return out;
|
||||
? schema.required.filter((item): item is string => typeof item === "string")
|
||||
: undefined
|
||||
const required = explicitRequired ?? inferredRequired
|
||||
const out: Record<string, unknown> = { type: "object" }
|
||||
if (Object.keys(properties).length > 0) out.properties = properties
|
||||
if (required.length > 0) out.required = required
|
||||
return out
|
||||
}
|
||||
case "array":
|
||||
case "Array":
|
||||
return {
|
||||
type: "array",
|
||||
items: toJsonSchema(schema.items ?? schema.element),
|
||||
};
|
||||
}
|
||||
case "union":
|
||||
case "Union": {
|
||||
const variants = Array.isArray(schema.variants)
|
||||
? schema.variants
|
||||
: Array.isArray(schema.anyOf)
|
||||
? schema.anyOf
|
||||
: [];
|
||||
: []
|
||||
for (const variant of variants) {
|
||||
const converted = toJsonSchema(variant);
|
||||
if (isRecord(converted) && Object.keys(converted).length > 0)
|
||||
return converted;
|
||||
const converted = toJsonSchema(variant)
|
||||
if (isRecord(converted) && Object.keys(converted).length > 0) return converted
|
||||
}
|
||||
return {};
|
||||
return {}
|
||||
}
|
||||
case "optional":
|
||||
case "Optional":
|
||||
return toJsonSchema(schema.wrapped ?? schema.inner);
|
||||
return toJsonSchema(schema.wrapped ?? schema.inner)
|
||||
default:
|
||||
return {};
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
export function toolsToJson(tools?: readonly ToolLike[]): unknown[] {
|
||||
if (!tools) return [];
|
||||
if (!tools) return []
|
||||
return tools.map((tool) => ({
|
||||
type: "function",
|
||||
name: tool.name,
|
||||
description: tool.description,
|
||||
input_schema: tool.parameters ? toJsonSchema(tool.parameters) : {},
|
||||
}));
|
||||
}))
|
||||
}
|
||||
|
||||
function completeToolCallIds(messages?: readonly MessageLike[]): Set<string> {
|
||||
const callIds = new Set<string>();
|
||||
const resultIds = new Set<string>();
|
||||
const callIds = new Set<string>()
|
||||
const resultIds = new Set<string>()
|
||||
|
||||
for (const message of messages ?? []) {
|
||||
if (message.role === "assistant") {
|
||||
for (const content of recordArray(message.content)) {
|
||||
if (content.type === "toolCall") {
|
||||
const id = stringValue(content.id);
|
||||
if (id) callIds.add(id);
|
||||
const id = stringValue(content.id)
|
||||
if (id) callIds.add(id)
|
||||
}
|
||||
}
|
||||
} else if (message.role === "toolResult") {
|
||||
if (message.toolCallId) resultIds.add(message.toolCallId);
|
||||
if (message.toolCallId) resultIds.add(message.toolCallId)
|
||||
}
|
||||
}
|
||||
|
||||
return new Set([...callIds].filter((id) => resultIds.has(id)));
|
||||
return new Set([...callIds].filter((id) => resultIds.has(id)))
|
||||
}
|
||||
|
||||
export function messagesToCC(messages?: readonly MessageLike[]): unknown[] {
|
||||
const out: unknown[] = [];
|
||||
const pairedToolCallIds = completeToolCallIds(messages);
|
||||
const out: unknown[] = []
|
||||
const pairedToolCallIds = completeToolCallIds(messages)
|
||||
|
||||
for (const message of messages ?? []) {
|
||||
if (message.role === "user") {
|
||||
out.push({
|
||||
role: "user",
|
||||
content:
|
||||
typeof message.content === "string"
|
||||
? message.content
|
||||
: message.content,
|
||||
});
|
||||
content: typeof message.content === "string" ? message.content : message.content,
|
||||
})
|
||||
} else if (message.role === "assistant") {
|
||||
const parts: unknown[] = [];
|
||||
const parts: unknown[] = []
|
||||
for (const content of recordArray(message.content)) {
|
||||
if (content.type === "text") {
|
||||
parts.push({ type: "text", text: stringValue(content.text) ?? "" });
|
||||
parts.push({ type: "text", text: stringValue(content.text) ?? "" })
|
||||
} else if (content.type === "thinking") {
|
||||
parts.push({
|
||||
type: "reasoning",
|
||||
text: stringValue(content.thinking) ?? "",
|
||||
});
|
||||
})
|
||||
} else if (content.type === "toolCall") {
|
||||
const toolCallId = stringValue(content.id) ?? "";
|
||||
if (!pairedToolCallIds.has(toolCallId)) continue;
|
||||
const toolCallId = stringValue(content.id) ?? ""
|
||||
if (!pairedToolCallIds.has(toolCallId)) continue
|
||||
parts.push({
|
||||
type: "tool-call",
|
||||
toolCallId,
|
||||
toolName: stringValue(content.name) ?? "",
|
||||
input: recordOrEmpty(content.arguments),
|
||||
});
|
||||
})
|
||||
}
|
||||
}
|
||||
if (parts.length > 0) out.push({ role: "assistant", content: parts });
|
||||
if (parts.length > 0) out.push({ role: "assistant", content: parts })
|
||||
} else if (message.role === "toolResult") {
|
||||
if (!message.toolCallId || !pairedToolCallIds.has(message.toolCallId))
|
||||
continue;
|
||||
if (!message.toolCallId || !pairedToolCallIds.has(message.toolCallId)) continue
|
||||
out.push({
|
||||
role: "tool",
|
||||
content: [
|
||||
@@ -265,36 +242,35 @@ export function messagesToCC(messages?: readonly MessageLike[]): unknown[] {
|
||||
: { type: "text", value: textContent(message) },
|
||||
},
|
||||
],
|
||||
});
|
||||
})
|
||||
}
|
||||
}
|
||||
return out;
|
||||
return out
|
||||
}
|
||||
|
||||
export function parseStreamEventLine(line: string): unknown | undefined {
|
||||
let trimmed = line.trim();
|
||||
if (!trimmed || trimmed.startsWith(":") || trimmed.startsWith("event:"))
|
||||
return undefined;
|
||||
if (trimmed.startsWith("data:")) trimmed = trimmed.slice(5).trim();
|
||||
if (!trimmed || trimmed === "[DONE]") return undefined;
|
||||
let trimmed = line.trim()
|
||||
if (!trimmed || trimmed.startsWith(":") || trimmed.startsWith("event:")) return undefined
|
||||
if (trimmed.startsWith("data:")) trimmed = trimmed.slice(5).trim()
|
||||
if (!trimmed || trimmed === "[DONE]") return undefined
|
||||
|
||||
try {
|
||||
const parsed: unknown = JSON.parse(trimmed);
|
||||
return parsed;
|
||||
const parsed: unknown = JSON.parse(trimmed)
|
||||
return parsed
|
||||
} catch {
|
||||
return undefined;
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
export function mapFinishReason(reason: unknown): StopReason {
|
||||
if (reason === "tool-calls") return "toolUse";
|
||||
if (reason === "tool-calls") return "toolUse"
|
||||
if (
|
||||
reason === "length" ||
|
||||
reason === "max_tokens" ||
|
||||
reason === "max-tokens" ||
|
||||
reason === "max_output_tokens"
|
||||
) {
|
||||
return "length";
|
||||
return "length"
|
||||
}
|
||||
return "stop";
|
||||
return "stop"
|
||||
}
|
||||
|
||||
+143
-163
@@ -5,7 +5,7 @@
|
||||
* dependencies so tests can exercise the real serialization and stream parser.
|
||||
*/
|
||||
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { randomUUID } from "node:crypto"
|
||||
|
||||
import {
|
||||
getApiKey,
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
recordOrEmpty,
|
||||
stringValue,
|
||||
toolsToJson,
|
||||
} from "./converters.ts";
|
||||
} from "./converters.ts"
|
||||
import type {
|
||||
AssistantMessageEventStreamLike,
|
||||
AssistantMessageLike,
|
||||
@@ -32,12 +32,12 @@ import type {
|
||||
TextContent,
|
||||
ToolCallContent,
|
||||
Usage,
|
||||
} from "./types.ts";
|
||||
} from "./types.ts"
|
||||
|
||||
export * from "./converters.ts";
|
||||
export * from "./types.ts";
|
||||
export * from "./converters.ts"
|
||||
export * from "./types.ts"
|
||||
|
||||
export const DEFAULT_API_BASE = "https://api.commandcode.ai";
|
||||
export const DEFAULT_API_BASE = "https://api.commandcode.ai"
|
||||
|
||||
function defaultUsage(): Usage {
|
||||
return {
|
||||
@@ -47,64 +47,60 @@ function defaultUsage(): Usage {
|
||||
cacheWrite: 0,
|
||||
totalTokens: 0,
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function commandCodeUsage(
|
||||
event: Record<string, unknown>,
|
||||
): Record<string, unknown> | undefined {
|
||||
return isRecord(event.totalUsage) ? event.totalUsage : undefined;
|
||||
function commandCodeUsage(event: Record<string, unknown>): Record<string, unknown> | undefined {
|
||||
return isRecord(event.totalUsage) ? event.totalUsage : undefined
|
||||
}
|
||||
|
||||
function commandCodeInputTokenDetails(
|
||||
usage: Record<string, unknown>,
|
||||
): Record<string, unknown> | undefined {
|
||||
return isRecord(usage.inputTokenDetails)
|
||||
? usage.inputTokenDetails
|
||||
: undefined;
|
||||
return isRecord(usage.inputTokenDetails) ? usage.inputTokenDetails : undefined
|
||||
}
|
||||
|
||||
function headersToRecord(headers: Headers): Record<string, string> {
|
||||
const out: Record<string, string> = {};
|
||||
const out: Record<string, string> = {}
|
||||
headers.forEach((value, key) => {
|
||||
out[key] = value;
|
||||
});
|
||||
return out;
|
||||
out[key] = value
|
||||
})
|
||||
return out
|
||||
}
|
||||
|
||||
function abortError(message = "The operation was aborted"): DOMException {
|
||||
return new DOMException(message, "AbortError");
|
||||
return new DOMException(message, "AbortError")
|
||||
}
|
||||
|
||||
function successStopReason(reason: TerminalReason): StopReason {
|
||||
if (reason === "length" || reason === "toolUse") return reason;
|
||||
return "stop";
|
||||
if (reason === "length" || reason === "toolUse") return reason
|
||||
return "stop"
|
||||
}
|
||||
|
||||
export function createStreamCommandCode(deps: CoreDependencies) {
|
||||
const apiBase = deps.apiBase ?? DEFAULT_API_BASE;
|
||||
const fetchImpl = deps.fetchImpl ?? fetch;
|
||||
const cwd = deps.cwd ?? (() => process.cwd());
|
||||
const now = deps.now ?? (() => Date.now());
|
||||
const uuid = deps.uuid ?? (() => randomUUID());
|
||||
const apiBase = deps.apiBase ?? DEFAULT_API_BASE
|
||||
const fetchImpl = deps.fetchImpl ?? fetch
|
||||
const cwd = deps.cwd ?? (() => process.cwd())
|
||||
const now = deps.now ?? (() => Date.now())
|
||||
const uuid = deps.uuid ?? (() => randomUUID())
|
||||
|
||||
function raceAbort<T>(promise: Promise<T>, signal: AbortSignal): Promise<T> {
|
||||
if (signal.aborted) return Promise.reject(abortError());
|
||||
if (signal.aborted) return Promise.reject(abortError())
|
||||
|
||||
return new Promise<T>((resolve, reject) => {
|
||||
const onAbort = () => reject(abortError());
|
||||
signal.addEventListener("abort", onAbort, { once: true });
|
||||
const onAbort = () => reject(abortError())
|
||||
signal.addEventListener("abort", onAbort, { once: true })
|
||||
promise.then(
|
||||
(value) => {
|
||||
signal.removeEventListener("abort", onAbort);
|
||||
resolve(value);
|
||||
signal.removeEventListener("abort", onAbort)
|
||||
resolve(value)
|
||||
},
|
||||
(error: unknown) => {
|
||||
signal.removeEventListener("abort", onAbort);
|
||||
reject(error);
|
||||
signal.removeEventListener("abort", onAbort)
|
||||
reject(error)
|
||||
},
|
||||
);
|
||||
});
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
return function streamCommandCode(
|
||||
@@ -112,7 +108,7 @@ export function createStreamCommandCode(deps: CoreDependencies) {
|
||||
context: ContextLike,
|
||||
options?: StreamOptions,
|
||||
): AssistantMessageEventStreamLike {
|
||||
const stream = deps.createStream();
|
||||
const stream = deps.createStream()
|
||||
|
||||
async function run() {
|
||||
const apiKey =
|
||||
@@ -121,7 +117,7 @@ export function createStreamCommandCode(deps: CoreDependencies) {
|
||||
env: deps.env,
|
||||
authPaths: deps.authPaths,
|
||||
homeDir: deps.homeDir,
|
||||
});
|
||||
})
|
||||
|
||||
if (!apiKey) {
|
||||
const msg: AssistantMessageLike = {
|
||||
@@ -135,10 +131,10 @@ export function createStreamCommandCode(deps: CoreDependencies) {
|
||||
errorMessage:
|
||||
"No Command Code API key. Run /login commandcode, set COMMANDCODE_API_KEY env var, or configure ~/.commandcode/auth.json or ~/.pi/agent/auth.json.",
|
||||
timestamp: now(),
|
||||
};
|
||||
stream.push({ type: "error", reason: "error", error: msg });
|
||||
stream.end();
|
||||
return;
|
||||
}
|
||||
stream.push({ type: "error", reason: "error", error: msg })
|
||||
stream.end()
|
||||
return
|
||||
}
|
||||
|
||||
const output: AssistantMessageLike = {
|
||||
@@ -150,168 +146,162 @@ export function createStreamCommandCode(deps: CoreDependencies) {
|
||||
usage: defaultUsage(),
|
||||
stopReason: "stop",
|
||||
timestamp: now(),
|
||||
};
|
||||
}
|
||||
|
||||
const controller = new AbortController();
|
||||
let reader: ReadableStreamDefaultReader<Uint8Array> | undefined;
|
||||
let textBlock: TextContent | undefined;
|
||||
let currentTextIdx = -1;
|
||||
let thinkingBlock: string[] = [];
|
||||
let finished = false;
|
||||
const controller = new AbortController()
|
||||
let reader: ReadableStreamDefaultReader<Uint8Array> | undefined
|
||||
let textBlock: TextContent | undefined
|
||||
let currentTextIdx = -1
|
||||
let thinkingBlock: string[] = []
|
||||
let finished = false
|
||||
|
||||
const abortUpstream = () => {
|
||||
if (!controller.signal.aborted) controller.abort();
|
||||
if (!controller.signal.aborted) controller.abort()
|
||||
try {
|
||||
reader?.cancel().catch(() => undefined);
|
||||
reader?.cancel().catch(() => undefined)
|
||||
} catch {
|
||||
// Reader cancellation is best-effort.
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if (options?.signal?.aborted) {
|
||||
abortUpstream();
|
||||
abortUpstream()
|
||||
} else {
|
||||
options?.signal?.addEventListener("abort", abortUpstream, {
|
||||
once: true,
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
const endTextBlock = () => {
|
||||
if (!textBlock) return;
|
||||
if (!textBlock) return
|
||||
stream.push({
|
||||
type: "text_end",
|
||||
contentIndex: currentTextIdx,
|
||||
content: textBlock.text,
|
||||
partial: output,
|
||||
});
|
||||
textBlock = undefined;
|
||||
currentTextIdx = -1;
|
||||
};
|
||||
})
|
||||
textBlock = undefined
|
||||
currentTextIdx = -1
|
||||
}
|
||||
|
||||
const flushThinkingBlock = () => {
|
||||
if (thinkingBlock.length === 0) return;
|
||||
const thinkingText = thinkingBlock.join("");
|
||||
thinkingBlock = [];
|
||||
output.content.push({ type: "thinking", thinking: thinkingText });
|
||||
const idx = output.content.length - 1;
|
||||
if (thinkingBlock.length === 0) return
|
||||
const thinkingText = thinkingBlock.join("")
|
||||
thinkingBlock = []
|
||||
output.content.push({ type: "thinking", thinking: thinkingText })
|
||||
const idx = output.content.length - 1
|
||||
stream.push({
|
||||
type: "thinking_start",
|
||||
contentIndex: idx,
|
||||
partial: output,
|
||||
});
|
||||
})
|
||||
stream.push({
|
||||
type: "thinking_delta",
|
||||
contentIndex: idx,
|
||||
delta: thinkingText,
|
||||
partial: output,
|
||||
});
|
||||
})
|
||||
stream.push({
|
||||
type: "thinking_end",
|
||||
contentIndex: idx,
|
||||
content: thinkingText,
|
||||
partial: output,
|
||||
});
|
||||
};
|
||||
})
|
||||
}
|
||||
|
||||
const handleEvent = (event: unknown) => {
|
||||
if (!isRecord(event)) return;
|
||||
if (!isRecord(event)) return
|
||||
|
||||
switch (event.type) {
|
||||
case "text-delta": {
|
||||
if (!textBlock) {
|
||||
textBlock = { type: "text", text: "" };
|
||||
output.content.push(textBlock);
|
||||
currentTextIdx = output.content.length - 1;
|
||||
textBlock = { type: "text", text: "" }
|
||||
output.content.push(textBlock)
|
||||
currentTextIdx = output.content.length - 1
|
||||
stream.push({
|
||||
type: "text_start",
|
||||
contentIndex: currentTextIdx,
|
||||
partial: output,
|
||||
});
|
||||
})
|
||||
}
|
||||
const delta = stringValue(event.text) ?? "";
|
||||
textBlock.text += delta;
|
||||
const delta = stringValue(event.text) ?? ""
|
||||
textBlock.text += delta
|
||||
stream.push({
|
||||
type: "text_delta",
|
||||
contentIndex: currentTextIdx,
|
||||
delta,
|
||||
partial: output,
|
||||
});
|
||||
break;
|
||||
})
|
||||
break
|
||||
}
|
||||
|
||||
case "reasoning-delta": {
|
||||
thinkingBlock.push(stringValue(event.text) ?? "");
|
||||
break;
|
||||
thinkingBlock.push(stringValue(event.text) ?? "")
|
||||
break
|
||||
}
|
||||
|
||||
case "reasoning-end": {
|
||||
flushThinkingBlock();
|
||||
break;
|
||||
flushThinkingBlock()
|
||||
break
|
||||
}
|
||||
|
||||
case "tool-call": {
|
||||
endTextBlock();
|
||||
endTextBlock()
|
||||
const toolCall: ToolCallContent = {
|
||||
type: "toolCall",
|
||||
id: stringValue(event.toolCallId) ?? "",
|
||||
name: stringValue(event.toolName) ?? "",
|
||||
arguments: recordOrEmpty(
|
||||
event.input ?? event.args ?? event.arguments,
|
||||
),
|
||||
};
|
||||
output.content.push(toolCall);
|
||||
const idx = output.content.length - 1;
|
||||
arguments: recordOrEmpty(event.input ?? event.args ?? event.arguments),
|
||||
}
|
||||
output.content.push(toolCall)
|
||||
const idx = output.content.length - 1
|
||||
stream.push({
|
||||
type: "toolcall_start",
|
||||
contentIndex: idx,
|
||||
partial: output,
|
||||
});
|
||||
})
|
||||
stream.push({
|
||||
type: "toolcall_end",
|
||||
contentIndex: idx,
|
||||
toolCall,
|
||||
partial: output,
|
||||
});
|
||||
break;
|
||||
})
|
||||
break
|
||||
}
|
||||
|
||||
case "finish": {
|
||||
const usage = commandCodeUsage(event);
|
||||
const usage = commandCodeUsage(event)
|
||||
if (usage) {
|
||||
const details = commandCodeInputTokenDetails(usage);
|
||||
output.usage.input = numberValue(usage.inputTokens) ?? 0;
|
||||
output.usage.output = numberValue(usage.outputTokens) ?? 0;
|
||||
output.usage.cacheRead =
|
||||
numberValue(details?.cacheReadTokens) ?? 0;
|
||||
output.usage.cacheWrite =
|
||||
numberValue(details?.cacheWriteTokens) ?? 0;
|
||||
const details = commandCodeInputTokenDetails(usage)
|
||||
output.usage.input = numberValue(usage.inputTokens) ?? 0
|
||||
output.usage.output = numberValue(usage.outputTokens) ?? 0
|
||||
output.usage.cacheRead = numberValue(details?.cacheReadTokens) ?? 0
|
||||
output.usage.cacheWrite = numberValue(details?.cacheWriteTokens) ?? 0
|
||||
output.usage.totalTokens =
|
||||
output.usage.input +
|
||||
output.usage.output +
|
||||
output.usage.cacheRead +
|
||||
output.usage.cacheWrite;
|
||||
deps.calculateCost(model, output.usage);
|
||||
output.usage.cacheWrite
|
||||
deps.calculateCost(model, output.usage)
|
||||
}
|
||||
output.stopReason = mapFinishReason(event.finishReason);
|
||||
finished = true;
|
||||
break;
|
||||
output.stopReason = mapFinishReason(event.finishReason)
|
||||
finished = true
|
||||
break
|
||||
}
|
||||
|
||||
case "error": {
|
||||
const errorRecord = isRecord(event.error) ? event.error : undefined;
|
||||
const errorRecord = isRecord(event.error) ? event.error : undefined
|
||||
const message =
|
||||
stringValue(errorRecord?.message) ??
|
||||
stringValue(event.error) ??
|
||||
"Stream error";
|
||||
output.stopReason = "error";
|
||||
output.errorMessage = message;
|
||||
throw new Error(message);
|
||||
stringValue(errorRecord?.message) ?? stringValue(event.error) ?? "Stream error"
|
||||
output.stopReason = "error"
|
||||
output.errorMessage = message
|
||||
throw new Error(message)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
stream.push({ type: "start", partial: output });
|
||||
stream.push({ type: "start", partial: output })
|
||||
|
||||
let body: unknown = {
|
||||
config: {
|
||||
@@ -334,19 +324,16 @@ export function createStreamCommandCode(deps: CoreDependencies) {
|
||||
messages: messagesToCC(context.messages),
|
||||
tools: toolsToJson(context.tools),
|
||||
system: context.systemPrompt ?? "",
|
||||
max_tokens: Math.min(
|
||||
options?.maxTokens ?? model.maxTokens,
|
||||
200_000,
|
||||
),
|
||||
max_tokens: Math.min(options?.maxTokens ?? model.maxTokens, 200_000),
|
||||
stream: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const nextBody = await raceAbort(
|
||||
Promise.resolve(options?.onPayload?.(body, model)),
|
||||
controller.signal,
|
||||
);
|
||||
if (nextBody !== undefined) body = nextBody;
|
||||
)
|
||||
if (nextBody !== undefined) body = nextBody
|
||||
|
||||
const response = await raceAbort(
|
||||
fetchImpl(`${apiBase}/alpha/generate`, {
|
||||
@@ -366,7 +353,7 @@ export function createStreamCommandCode(deps: CoreDependencies) {
|
||||
signal: controller.signal,
|
||||
}),
|
||||
controller.signal,
|
||||
);
|
||||
)
|
||||
|
||||
await raceAbort(
|
||||
Promise.resolve(
|
||||
@@ -379,78 +366,71 @@ export function createStreamCommandCode(deps: CoreDependencies) {
|
||||
),
|
||||
),
|
||||
controller.signal,
|
||||
);
|
||||
)
|
||||
|
||||
if (!response.ok) {
|
||||
const errBody = await raceAbort(
|
||||
response.text().catch(() => ""),
|
||||
controller.signal,
|
||||
);
|
||||
throw new Error(
|
||||
`Command Code API error ${response.status}: ${errBody.slice(0, 500)}`,
|
||||
);
|
||||
)
|
||||
throw new Error(`Command Code API error ${response.status}: ${errBody.slice(0, 500)}`)
|
||||
}
|
||||
|
||||
reader = response.body?.getReader();
|
||||
if (!reader) throw new Error("No response body");
|
||||
reader = response.body?.getReader()
|
||||
if (!reader) throw new Error("No response body")
|
||||
|
||||
const decoder = new TextDecoder();
|
||||
let buffer = "";
|
||||
const decoder = new TextDecoder()
|
||||
let buffer = ""
|
||||
|
||||
readLoop: for (;;) {
|
||||
if (controller.signal.aborted) throw abortError("Aborted");
|
||||
const { done, value } = await raceAbort(
|
||||
reader.read(),
|
||||
controller.signal,
|
||||
);
|
||||
if (controller.signal.aborted) throw abortError("Aborted")
|
||||
const { done, value } = await raceAbort(reader.read(), controller.signal)
|
||||
if (done) {
|
||||
if (buffer.trim()) handleEvent(parseStreamEventLine(buffer));
|
||||
break;
|
||||
if (buffer.trim()) handleEvent(parseStreamEventLine(buffer))
|
||||
break
|
||||
}
|
||||
if (controller.signal.aborted) throw abortError("Aborted");
|
||||
if (controller.signal.aborted) throw abortError("Aborted")
|
||||
|
||||
buffer += decoder.decode(value, { stream: true });
|
||||
const lines = buffer.split("\n");
|
||||
buffer = lines.pop() ?? "";
|
||||
buffer += decoder.decode(value, { stream: true })
|
||||
const lines = buffer.split("\n")
|
||||
buffer = lines.pop() ?? ""
|
||||
|
||||
for (const line of lines) {
|
||||
if (controller.signal.aborted) throw abortError("Aborted");
|
||||
handleEvent(parseStreamEventLine(line));
|
||||
if (finished) break readLoop;
|
||||
if (controller.signal.aborted) throw abortError("Aborted")
|
||||
handleEvent(parseStreamEventLine(line))
|
||||
if (finished) break readLoop
|
||||
}
|
||||
}
|
||||
|
||||
endTextBlock();
|
||||
flushThinkingBlock();
|
||||
endTextBlock()
|
||||
flushThinkingBlock()
|
||||
|
||||
stream.push({
|
||||
type: "done",
|
||||
reason: successStopReason(output.stopReason),
|
||||
message: output,
|
||||
});
|
||||
stream.end();
|
||||
})
|
||||
stream.end()
|
||||
} catch (error: unknown) {
|
||||
const reason: ErrorReason = controller.signal.aborted
|
||||
? "aborted"
|
||||
: "error";
|
||||
output.stopReason = reason;
|
||||
const reason: ErrorReason = controller.signal.aborted ? "aborted" : "error"
|
||||
output.stopReason = reason
|
||||
output.errorMessage =
|
||||
reason === "aborted"
|
||||
? "Request aborted"
|
||||
: error instanceof Error
|
||||
? error.message
|
||||
: String(error);
|
||||
stream.push({ type: "error", reason, error: output });
|
||||
stream.end();
|
||||
: String(error)
|
||||
stream.push({ type: "error", reason, error: output })
|
||||
stream.end()
|
||||
} finally {
|
||||
options?.signal?.removeEventListener("abort", abortUpstream);
|
||||
options?.signal?.removeEventListener("abort", abortUpstream)
|
||||
try {
|
||||
await reader?.cancel();
|
||||
await reader?.cancel()
|
||||
} catch {
|
||||
// Reader may already be closed/cancelled.
|
||||
}
|
||||
try {
|
||||
reader?.releaseLock();
|
||||
reader?.releaseLock()
|
||||
} catch {
|
||||
// Reader may already be released/cancelled by the abort path.
|
||||
}
|
||||
@@ -468,11 +448,11 @@ export function createStreamCommandCode(deps: CoreDependencies) {
|
||||
stopReason: "error",
|
||||
errorMessage: error instanceof Error ? error.message : String(error),
|
||||
timestamp: now(),
|
||||
};
|
||||
stream.push({ type: "error", reason: "error", error: msg });
|
||||
stream.end();
|
||||
});
|
||||
}
|
||||
stream.push({ type: "error", reason: "error", error: msg })
|
||||
stream.end()
|
||||
})
|
||||
|
||||
return stream;
|
||||
};
|
||||
return stream
|
||||
}
|
||||
}
|
||||
|
||||
+25
-30
@@ -12,25 +12,25 @@
|
||||
* OAuth credentials with a far-future expiry.
|
||||
*/
|
||||
|
||||
import { randomBytes } from "node:crypto";
|
||||
import { startAuthServer } from "./auth-server.ts";
|
||||
import { randomBytes } from "node:crypto"
|
||||
import { startAuthServer } from "./auth-server.ts"
|
||||
|
||||
const STUDIO_BASE_URL = "https://commandcode.ai";
|
||||
const TEN_YEARS_MS = 10 * 365 * 24 * 60 * 60 * 1000; // API keys don't expire
|
||||
const STUDIO_BASE_URL = "https://commandcode.ai"
|
||||
const TEN_YEARS_MS = 10 * 365 * 24 * 60 * 60 * 1000 // API keys don't expire
|
||||
|
||||
export interface OAuthLoginCallbacks {
|
||||
onAuth(params: { url: string }): void;
|
||||
onPrompt(params: { message: string }): Promise<string>;
|
||||
onAuth(params: { url: string }): void
|
||||
onPrompt(params: { message: string }): Promise<string>
|
||||
}
|
||||
|
||||
export interface OAuthCredentials {
|
||||
refresh: string;
|
||||
access: string;
|
||||
expires: number;
|
||||
refresh: string
|
||||
access: string
|
||||
expires: number
|
||||
}
|
||||
|
||||
function generateStateToken(): string {
|
||||
return randomBytes(32).toString("base64url");
|
||||
return randomBytes(32).toString("base64url")
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -39,32 +39,29 @@ function generateStateToken(): string {
|
||||
* Returns OAuth credentials where access == refresh == the user's API key.
|
||||
* The keys don't expire, so we set a far-future expiry.
|
||||
*/
|
||||
export async function login(
|
||||
callbacks: OAuthLoginCallbacks,
|
||||
): Promise<OAuthCredentials> {
|
||||
const authServer = await startAuthServer();
|
||||
const stateToken = generateStateToken();
|
||||
export async function login(callbacks: OAuthLoginCallbacks): Promise<OAuthCredentials> {
|
||||
const authServer = await startAuthServer()
|
||||
const stateToken = generateStateToken()
|
||||
|
||||
const authUrl = `${STUDIO_BASE_URL}/studio/auth/cli?callback=${encodeURIComponent(`http://localhost:${authServer.port}/callback`)}&state=${encodeURIComponent(stateToken)}`;
|
||||
const authUrl = `${STUDIO_BASE_URL}/studio/auth/cli?callback=${encodeURIComponent(`http://localhost:${authServer.port}/callback`)}&state=${encodeURIComponent(stateToken)}`
|
||||
|
||||
// Tell pi to open the browser
|
||||
callbacks.onAuth({ url: authUrl });
|
||||
callbacks.onAuth({ url: authUrl })
|
||||
|
||||
// Wait for the Command Code Studio to POST the API key back
|
||||
let callback: { apiKey: string; state: string };
|
||||
let callback: { apiKey: string; state: string }
|
||||
try {
|
||||
callback = await authServer.waitForCallback;
|
||||
callback = await authServer.waitForCallback
|
||||
} catch (error) {
|
||||
// Clean up server on error
|
||||
authServer.server.close();
|
||||
throw error;
|
||||
authServer.server.close()
|
||||
throw error
|
||||
}
|
||||
|
||||
// Validate state token to prevent CSRF
|
||||
if (callback.state !== stateToken) {
|
||||
throw new Error(
|
||||
"State token mismatch. Authentication may have been tampered with.",
|
||||
);
|
||||
authServer.server.close()
|
||||
throw new Error("State token mismatch. Authentication may have been tampered with.")
|
||||
}
|
||||
|
||||
// Return as OAuth credentials. Since CC API keys don't expire,
|
||||
@@ -73,26 +70,24 @@ export async function login(
|
||||
refresh: callback.apiKey,
|
||||
access: callback.apiKey,
|
||||
expires: Date.now() + TEN_YEARS_MS,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Command Code API keys don't expire, so "refresh" is a no-op.
|
||||
* Returns the same credentials with an updated far-future expiry.
|
||||
*/
|
||||
export async function refreshToken(
|
||||
credentials: OAuthCredentials,
|
||||
): Promise<OAuthCredentials> {
|
||||
export async function refreshToken(credentials: OAuthCredentials): Promise<OAuthCredentials> {
|
||||
return {
|
||||
refresh: credentials.refresh,
|
||||
access: credentials.access,
|
||||
expires: Date.now() + TEN_YEARS_MS,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the access token (API key) from OAuth credentials.
|
||||
*/
|
||||
export function getApiKey(credentials: OAuthCredentials): string {
|
||||
return credentials.access;
|
||||
return credentials.access
|
||||
}
|
||||
|
||||
+94
-100
@@ -1,162 +1,156 @@
|
||||
export type StopReason = "stop" | "length" | "toolUse";
|
||||
export type ErrorReason = "error" | "aborted";
|
||||
export type TerminalReason = StopReason | ErrorReason;
|
||||
export type StopReason = "stop" | "length" | "toolUse"
|
||||
export type ErrorReason = "error" | "aborted"
|
||||
export type TerminalReason = StopReason | ErrorReason
|
||||
|
||||
export interface UsageCost {
|
||||
input: number;
|
||||
output: number;
|
||||
cacheRead: number;
|
||||
cacheWrite: number;
|
||||
total: number;
|
||||
input: number
|
||||
output: number
|
||||
cacheRead: number
|
||||
cacheWrite: number
|
||||
total: number
|
||||
}
|
||||
|
||||
export interface Usage {
|
||||
input: number;
|
||||
output: number;
|
||||
cacheRead: number;
|
||||
cacheWrite: number;
|
||||
totalTokens: number;
|
||||
cost: UsageCost;
|
||||
input: number
|
||||
output: number
|
||||
cacheRead: number
|
||||
cacheWrite: number
|
||||
totalTokens: number
|
||||
cost: UsageCost
|
||||
}
|
||||
|
||||
export interface TextContent {
|
||||
type: "text";
|
||||
text: string;
|
||||
type: "text"
|
||||
text: string
|
||||
}
|
||||
|
||||
export interface ThinkingContent {
|
||||
type: "thinking";
|
||||
thinking: string;
|
||||
type: "thinking"
|
||||
thinking: string
|
||||
}
|
||||
|
||||
export interface ToolCallContent {
|
||||
type: "toolCall";
|
||||
id: string;
|
||||
name: string;
|
||||
arguments: Record<string, unknown>;
|
||||
type: "toolCall"
|
||||
id: string
|
||||
name: string
|
||||
arguments: Record<string, unknown>
|
||||
}
|
||||
|
||||
export type AssistantContent = TextContent | ThinkingContent | ToolCallContent;
|
||||
export type AssistantContent = TextContent | ThinkingContent | ToolCallContent
|
||||
|
||||
export interface AssistantMessageLike {
|
||||
role: "assistant";
|
||||
content: AssistantContent[];
|
||||
api: unknown;
|
||||
provider: string;
|
||||
model: string;
|
||||
usage: Usage;
|
||||
stopReason: TerminalReason;
|
||||
errorMessage?: string;
|
||||
timestamp: number;
|
||||
role: "assistant"
|
||||
content: AssistantContent[]
|
||||
api: unknown
|
||||
provider: string
|
||||
model: string
|
||||
usage: Usage
|
||||
stopReason: TerminalReason
|
||||
errorMessage?: string
|
||||
timestamp: number
|
||||
}
|
||||
|
||||
export interface ModelLike {
|
||||
id: string;
|
||||
api: unknown;
|
||||
provider: string;
|
||||
maxTokens: number;
|
||||
id: string
|
||||
api: unknown
|
||||
provider: string
|
||||
maxTokens: number
|
||||
}
|
||||
|
||||
export interface MessageLike {
|
||||
role: string;
|
||||
content?: unknown;
|
||||
toolCallId?: string;
|
||||
toolName?: string;
|
||||
isError?: boolean;
|
||||
role: string
|
||||
content?: unknown
|
||||
toolCallId?: string
|
||||
toolName?: string
|
||||
isError?: boolean
|
||||
}
|
||||
|
||||
export interface ToolLike {
|
||||
name: string;
|
||||
description?: string;
|
||||
parameters?: unknown;
|
||||
name: string
|
||||
description?: string
|
||||
parameters?: unknown
|
||||
}
|
||||
|
||||
export interface ContextLike {
|
||||
systemPrompt?: string;
|
||||
messages?: readonly MessageLike[];
|
||||
tools?: readonly ToolLike[];
|
||||
systemPrompt?: string
|
||||
messages?: readonly MessageLike[]
|
||||
tools?: readonly ToolLike[]
|
||||
}
|
||||
|
||||
export interface ProviderResponseInfo {
|
||||
status: number;
|
||||
headers: Record<string, string>;
|
||||
status: number
|
||||
headers: Record<string, string>
|
||||
}
|
||||
|
||||
export interface StreamOptions {
|
||||
apiKey?: string;
|
||||
signal?: AbortSignal;
|
||||
headers?: Record<string, string>;
|
||||
maxTokens?: number;
|
||||
onPayload?: (
|
||||
payload: unknown,
|
||||
model: ModelLike,
|
||||
) => unknown | Promise<unknown>;
|
||||
onResponse?: (
|
||||
response: ProviderResponseInfo,
|
||||
model: ModelLike,
|
||||
) => void | Promise<void>;
|
||||
apiKey?: string
|
||||
signal?: AbortSignal
|
||||
headers?: Record<string, string>
|
||||
maxTokens?: number
|
||||
onPayload?: (payload: unknown, model: ModelLike) => unknown | Promise<unknown>
|
||||
onResponse?: (response: ProviderResponseInfo, model: ModelLike) => void | Promise<void>
|
||||
}
|
||||
|
||||
export type AssistantMessageEvent =
|
||||
| { type: "start"; partial: AssistantMessageLike }
|
||||
| { type: "text_start"; contentIndex: number; partial: AssistantMessageLike }
|
||||
| {
|
||||
type: "text_delta";
|
||||
contentIndex: number;
|
||||
delta: string;
|
||||
partial: AssistantMessageLike;
|
||||
type: "text_delta"
|
||||
contentIndex: number
|
||||
delta: string
|
||||
partial: AssistantMessageLike
|
||||
}
|
||||
| {
|
||||
type: "text_end";
|
||||
contentIndex: number;
|
||||
content: string;
|
||||
partial: AssistantMessageLike;
|
||||
type: "text_end"
|
||||
contentIndex: number
|
||||
content: string
|
||||
partial: AssistantMessageLike
|
||||
}
|
||||
| {
|
||||
type: "thinking_start";
|
||||
contentIndex: number;
|
||||
partial: AssistantMessageLike;
|
||||
type: "thinking_start"
|
||||
contentIndex: number
|
||||
partial: AssistantMessageLike
|
||||
}
|
||||
| {
|
||||
type: "thinking_delta";
|
||||
contentIndex: number;
|
||||
delta: string;
|
||||
partial: AssistantMessageLike;
|
||||
type: "thinking_delta"
|
||||
contentIndex: number
|
||||
delta: string
|
||||
partial: AssistantMessageLike
|
||||
}
|
||||
| {
|
||||
type: "thinking_end";
|
||||
contentIndex: number;
|
||||
content: string;
|
||||
partial: AssistantMessageLike;
|
||||
type: "thinking_end"
|
||||
contentIndex: number
|
||||
content: string
|
||||
partial: AssistantMessageLike
|
||||
}
|
||||
| {
|
||||
type: "toolcall_start";
|
||||
contentIndex: number;
|
||||
partial: AssistantMessageLike;
|
||||
type: "toolcall_start"
|
||||
contentIndex: number
|
||||
partial: AssistantMessageLike
|
||||
}
|
||||
| {
|
||||
type: "toolcall_end";
|
||||
contentIndex: number;
|
||||
toolCall: ToolCallContent;
|
||||
partial: AssistantMessageLike;
|
||||
type: "toolcall_end"
|
||||
contentIndex: number
|
||||
toolCall: ToolCallContent
|
||||
partial: AssistantMessageLike
|
||||
}
|
||||
| { type: "done"; reason: StopReason; message: AssistantMessageLike }
|
||||
| { type: "error"; reason: ErrorReason; error: AssistantMessageLike };
|
||||
| { type: "error"; reason: ErrorReason; error: AssistantMessageLike }
|
||||
|
||||
export interface AssistantMessageEventStreamLike extends AsyncIterable<AssistantMessageEvent> {
|
||||
push(event: AssistantMessageEvent): void;
|
||||
end(): void;
|
||||
push(event: AssistantMessageEvent): void
|
||||
end(): void
|
||||
}
|
||||
|
||||
export interface CoreDependencies {
|
||||
createStream: () => AssistantMessageEventStreamLike;
|
||||
calculateCost: (model: ModelLike, usage: Usage) => void;
|
||||
apiBase?: string;
|
||||
fetchImpl?: typeof fetch;
|
||||
authPaths?: readonly string[];
|
||||
env?: NodeJS.ProcessEnv;
|
||||
cwd?: () => string;
|
||||
now?: () => number;
|
||||
uuid?: () => string;
|
||||
homeDir?: () => string;
|
||||
createStream: () => AssistantMessageEventStreamLike
|
||||
calculateCost: (model: ModelLike, usage: Usage) => void
|
||||
apiBase?: string
|
||||
fetchImpl?: typeof fetch
|
||||
authPaths?: readonly string[]
|
||||
env?: NodeJS.ProcessEnv
|
||||
cwd?: () => string
|
||||
now?: () => number
|
||||
uuid?: () => string
|
||||
homeDir?: () => string
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user