style: configure prettier (no semicolons, trailing commas)

This commit is contained in:
Patrick Wozniak
2026-05-05 14:02:41 +02:00
parent e9853eb4a4
commit af2b316d84
14 changed files with 1247 additions and 1446 deletions
+68 -74
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
}