chore: add CI workflow (typecheck + prettier format check), tsconfig, and prettier formatting

This commit is contained in:
Patrick Wozniak
2026-05-05 13:56:41 +02:00
parent be6d17ea56
commit e9853eb4a4
18 changed files with 1488 additions and 250 deletions
+91 -19
View File
@@ -16,17 +16,30 @@ function booleanValue(value: unknown): boolean | undefined {
return typeof value === "boolean" ? value : undefined;
}
export function recordArray(value: unknown): readonly Record<string, unknown>[] {
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> {
return isRecord(value) ? value : {};
if (isRecord(value)) return value;
if (typeof value === "string") {
try {
const parsed: unknown = JSON.parse(value);
if (isRecord(parsed)) return parsed;
} catch {
// Some providers stream incomplete JSON argument fragments.
}
}
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[] {
@@ -36,11 +49,13 @@ function defaultAuthPaths(home: string): string[] {
];
}
export function getApiKey(options: {
env?: NodeJS.ProcessEnv;
authPaths?: readonly string[];
homeDir?: () => string;
} = {}): string | undefined {
export function getApiKey(
options: {
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;
@@ -52,10 +67,21 @@ export function getApiKey(options: {
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;
// OAuth: pi stores OAuth credentials as {"commandcode": {"type":"oauth","access":"...","refresh":"...","expires":...}}
const providerKey = isRecord(parsed.commandcode)
? parsed.commandcode
: undefined;
if (providerKey && stringValue(providerKey.type) === "oauth") {
const access = stringValue(providerKey.access);
if (access) return access;
}
} catch {
// Ignore malformed or unreadable auth files.
}
@@ -98,23 +124,32 @@ export function toJsonSchema(schema: unknown): unknown {
case "Object": {
const properties: Record<string, unknown> = {};
const inferredRequired: string[] = [];
const sourceProperties = isRecord(schema.properties) ? schema.properties : undefined;
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)) {
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")
? schema.required.filter(
(item): item is string => typeof item === "string",
)
: undefined;
const required = explicitRequired ?? inferredRequired;
const out: Record<string, unknown> = { type: "object" };
@@ -124,7 +159,10 @@ export function toJsonSchema(schema: unknown): unknown {
}
case "array":
case "Array":
return { type: "array", items: toJsonSchema(schema.items ?? schema.element) };
return {
type: "array",
items: toJsonSchema(schema.items ?? schema.element),
};
case "union":
case "Union": {
const variants = Array.isArray(schema.variants)
@@ -134,7 +172,8 @@ export function toJsonSchema(schema: unknown): unknown {
: [];
for (const variant of variants) {
const converted = toJsonSchema(variant);
if (isRecord(converted) && Object.keys(converted).length > 0) return converted;
if (isRecord(converted) && Object.keys(converted).length > 0)
return converted;
}
return {};
}
@@ -156,13 +195,38 @@ export function toolsToJson(tools?: readonly ToolLike[]): unknown[] {
}));
}
function completeToolCallIds(messages?: readonly MessageLike[]): 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);
}
}
} else if (message.role === "toolResult") {
if (message.toolCallId) resultIds.add(message.toolCallId);
}
}
return new Set([...callIds].filter((id) => resultIds.has(id)));
}
export function messagesToCC(messages?: readonly MessageLike[]): unknown[] {
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[] = [];
@@ -170,18 +234,25 @@ export function messagesToCC(messages?: readonly MessageLike[]): unknown[] {
if (content.type === "text") {
parts.push({ type: "text", text: stringValue(content.text) ?? "" });
} else if (content.type === "thinking") {
parts.push({ type: "reasoning", text: stringValue(content.thinking) ?? "" });
parts.push({
type: "reasoning",
text: stringValue(content.thinking) ?? "",
});
} else if (content.type === "toolCall") {
const toolCallId = stringValue(content.id) ?? "";
if (!pairedToolCallIds.has(toolCallId)) continue;
parts.push({
type: "tool-call",
toolCallId: stringValue(content.id) ?? "",
toolCallId,
toolName: stringValue(content.name) ?? "",
input: recordOrEmpty(content.arguments),
});
}
}
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;
out.push({
role: "tool",
content: [
@@ -202,7 +273,8 @@ export function messagesToCC(messages?: readonly MessageLike[]): unknown[] {
export function parseStreamEventLine(line: string): unknown | undefined {
let trimmed = line.trim();
if (!trimmed || trimmed.startsWith(":") || trimmed.startsWith("event:")) return undefined;
if (!trimmed || trimmed.startsWith(":") || trimmed.startsWith("event:"))
return undefined;
if (trimmed.startsWith("data:")) trimmed = trimmed.slice(5).trim();
if (!trimmed || trimmed === "[DONE]") return undefined;