refactor: eliminate ~400 no-explicit-any warnings across the workspace

Parallel subagent pass: four typescript-pro agents on non-overlapping scopes.

Patterns applied:
- catch (err: any) { ... err.message ... } → catch (err) { ... getErrorMessage(err) ... }
  using the new @fusion/core helper. Bare catch {} where the error was unused.
- SQLite row types: defined typed XxxRow interfaces per table and cast
  .all()/.get() results via `as unknown as XxxRow[]` (the double cast is
  required because better-sqlite3 returns Record<string, SQLOutputValue>).
- rowToX(row: any) converters: typed argument with the matching row interface.
- Dynamic settings key writes: (settings as Record<string, unknown>)[key].
- React event handlers and setState callbacks: inferred types or concrete
  React.{Mouse,Change,Form}Event<...> where needed.
- pi-claude-cli: local PiMessage / PiContext duck types to avoid re-typing
  pi-ai concrete shapes; typed Claude stream event message fields.

72 files changed, ~400 anys eliminated. Typecheck passes across the workspace.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsxdsm
2026-04-23 18:57:31 -07:00
parent e3b6965620
commit 3fbb7c47cf
72 changed files with 1298 additions and 783 deletions

View File

@@ -11,6 +11,19 @@
import { existsSync, readFileSync } from "node:fs";
import { resolve, join, dirname } from "node:path";
import { homedir } from "node:os";
/**
* Minimal message shape that prompt-builder accepts.
* Uses a wide `role: string` discriminant so tests can pass plain objects
* without literal type annotations. Content is typed broadly as
* `string | unknown[]` since helper functions narrow at runtime.
*/
export interface PiMessage {
role: string;
content: string | unknown[];
toolName?: string;
}
export type PiContext = { systemPrompt?: string; messages: PiMessage[] };
import {
mapPiToolNameToClaude,
translatePiArgsToClaude,
@@ -29,9 +42,6 @@ type AnthropicContentBlock =
source: { type: "base64"; media_type: string; data: string };
};
// We use `any` for Context to avoid requiring @mariozechner/pi-ai at dev time.
// At runtime, pi provides the real Context type.
/**
* Flattens a pi conversation context's messages array into a labeled text prompt
* suitable for sending to the Claude CLI subprocess.
@@ -51,14 +61,15 @@ let placeholderImageCount = 0;
* pi-ai format: { type: "image", data: string (base64), mimeType: string }
* Anthropic format: { type: "image", source: { type: "base64", media_type: string, data: string } }
*/
function translateImageBlock(piBlock: any): AnthropicContentBlock | null {
if (piBlock.data && piBlock.mimeType) {
function translateImageBlock(piBlock: unknown): AnthropicContentBlock | null {
const block = piBlock as Record<string, unknown>;
if (typeof block.data === "string" && typeof block.mimeType === "string") {
return {
type: "image",
source: {
type: "base64",
media_type: piBlock.mimeType,
data: piBlock.data,
media_type: block.mimeType,
data: block.data,
},
};
}
@@ -72,7 +83,7 @@ function translateImageBlock(piBlock: any): AnthropicContentBlock | null {
* @returns Array of AnthropicContentBlock with text and translated images
*/
function buildFinalUserContent(
content: string | any[],
content: string | unknown[],
): AnthropicContentBlock[] {
if (typeof content === "string") {
return [{ type: "text", text: content }];
@@ -82,9 +93,10 @@ function buildFinalUserContent(
}
const blocks: AnthropicContentBlock[] = [];
for (const block of content) {
for (const rawBlock of content) {
const block = rawBlock as Record<string, unknown>;
if (block.type === "text") {
blocks.push({ type: "text", text: block.text ?? "" });
blocks.push({ type: "text", text: typeof block.text === "string" ? block.text : "" });
} else if (block.type === "image") {
const translated = translateImageBlock(block);
if (translated) {
@@ -106,9 +118,9 @@ function buildFinalUserContent(
/**
* Check if a message content array contains image blocks.
*/
function contentHasImages(content: string | any[]): boolean {
function contentHasImages(content: string | unknown[]): boolean {
if (typeof content === "string" || !Array.isArray(content)) return false;
return content.some((block) => block.type === "image");
return content.some((block) => (block as Record<string, unknown>).type === "image");
}
/**
@@ -116,7 +128,7 @@ function contentHasImages(content: string | any[]): boolean {
* If so, build a simplified prompt that presents the result directly
* instead of replaying the full conversation history with tool labels.
*/
function buildCustomToolResultPrompt(messages: any[]): string | null {
function buildCustomToolResultPrompt(messages: PiMessage[]): string | null {
if (messages.length < 3) return null;
const last = messages[messages.length - 1];
@@ -126,8 +138,9 @@ function buildCustomToolResultPrompt(messages: any[]): string | null {
// Find the original user message (scan backwards past assistant + toolResult)
let userMessage: string | null = null;
for (let i = messages.length - 3; i >= 0; i--) {
if (messages[i].role === "user") {
userMessage = userContentToText(messages[i].content);
const msg = messages[i];
if (msg.role === "user") {
userMessage = userContentToText(msg.content);
break;
}
}
@@ -150,9 +163,7 @@ function buildCustomToolResultPrompt(messages: any[]): string | null {
*
* Falls back to full prompt if the message structure is unexpected.
*/
export function buildResumePrompt(context: {
messages: any[];
}): string | AnthropicContentBlock[] {
export function buildResumePrompt(context: PiContext): string | AnthropicContentBlock[] {
const messages = context.messages;
if (messages.length === 0) return "";
@@ -162,7 +173,7 @@ export function buildResumePrompt(context: {
// Collect new messages: everything from the last assistant turn onwards
// (tool results from the last assistant + the new user message)
const newMessages: any[] = [];
const newMessages: PiMessage[] = [];
// Walk backwards from finalUserIndex to find where new content starts.
// Include trailing toolResult messages that follow the last assistant turn.
@@ -211,9 +222,7 @@ export function buildResumePrompt(context: {
return parts.join("\n") || "";
}
export function buildPrompt(context: {
messages: any[];
}): string | AnthropicContentBlock[] {
export function buildPrompt(context: PiContext): string | AnthropicContentBlock[] {
// Reset placeholder counter for each call
placeholderImageCount = 0;
@@ -232,11 +241,13 @@ export function buildPrompt(context: {
// Determine if any message has images worth passing through
const finalUserIndex = findFinalUserMessageIndex(context.messages);
const finalUserMsg = finalUserIndex >= 0 ? context.messages[finalUserIndex] : undefined;
const finalUserHasImages =
finalUserIndex >= 0 &&
contentHasImages(context.messages[finalUserIndex].content);
finalUserMsg !== undefined &&
finalUserMsg.role === "user" &&
contentHasImages(finalUserMsg.content);
const anyToolResultHasImages = context.messages.some(
(m: any) => m.role === "toolResult" && toolResultHasImages(m.content),
(m) => m.role === "toolResult" && toolResultHasImages(m.content),
);
if (finalUserHasImages || anyToolResultHasImages) {
@@ -265,7 +276,8 @@ export function buildPrompt(context: {
historyParts.push(toolResultContentToText(message.content));
// Collect image blocks from tool results for passthrough
if (Array.isArray(message.content)) {
for (const block of message.content) {
for (const rawBlock of message.content) {
const block = rawBlock as Record<string, unknown>;
if (block.type === "image") {
const translated = translateImageBlock(block);
if (translated) {
@@ -281,8 +293,8 @@ export function buildPrompt(context: {
// Build final user message content blocks
const finalUserContent =
finalUserIndex >= 0
? buildFinalUserContent(context.messages[finalUserIndex].content)
finalUserMsg?.role === "user"
? buildFinalUserContent(finalUserMsg.content)
: [];
// Combine: history text + tool result images + final user content blocks
@@ -341,7 +353,7 @@ export function buildPrompt(context: {
* Find the index of the last user message in the messages array.
* Returns -1 if no user message found.
*/
function findFinalUserMessageIndex(messages: any[]): number {
function findFinalUserMessageIndex(messages: PiMessage[]): number {
for (let i = messages.length - 1; i >= 0; i--) {
if (messages[i].role === "user") return i;
}
@@ -354,7 +366,7 @@ function findFinalUserMessageIndex(messages: any[]): number {
* Sanitizes .pi references to .claude for Claude Code compatibility.
*/
export function buildSystemPrompt(
context: { systemPrompt?: string; messages: any[] },
context: PiContext,
cwd: string,
): string {
const parts: string[] = [];
@@ -377,7 +389,7 @@ export function buildSystemPrompt(
// When conversation history has tool results, instruct Claude to use them
// instead of trying to re-call tools (which may not be available).
if (context.messages?.some((m: any) => m.role === "toolResult")) {
if (context.messages?.some((m) => m.role === "toolResult")) {
parts.push(
"IMPORTANT: The conversation history below contains tool results from previously executed tools. " +
"Use these results to answer the user's question. Do NOT attempt to re-call tools that already have results.",
@@ -393,14 +405,15 @@ export function buildSystemPrompt(
* Image blocks are replaced with placeholder text (HIST-02).
* Increments the module-level placeholderImageCount for each image.
*/
function userContentToText(content: string | any[]): string {
function userContentToText(content: string | unknown[]): string {
if (typeof content === "string") return content;
if (!Array.isArray(content)) return "";
const texts: string[] = [];
for (const block of content) {
for (const rawBlock of content) {
const block = rawBlock as Record<string, unknown>;
if (block.type === "text") {
texts.push(block.text ?? "");
texts.push(typeof block.text === "string" ? block.text : "");
} else if (block.type === "image") {
texts.push("[An image was shared here but could not be included]");
placeholderImageCount++;
@@ -414,37 +427,34 @@ function userContentToText(content: string | any[]): string {
* Converts assistant message content to text.
* Handles string content and array of content blocks (text, thinking, toolCall).
*/
function contentToText(content: string | any[]): string {
function contentToText(content: string | unknown[]): string {
if (typeof content === "string") return content;
if (!Array.isArray(content)) return "";
return content
.map((block) => {
if (block.type === "text") return block.text ?? "";
.map((rawBlock) => {
const block = rawBlock as Record<string, unknown>;
if (block.type === "text") return typeof block.text === "string" ? block.text : "";
if (block.type === "thinking") return ""; // Skip thinking — internal reasoning, not conversation
if (block.type === "toolCall") {
const isCustom = isCustomToolName(block.name);
const name = typeof block.name === "string" ? block.name : "";
const args = block.arguments && typeof block.arguments === "object"
? (block.arguments as Record<string, unknown>)
: undefined;
const isCustom = isCustomToolName(name);
if (isCustom) {
// Custom tools: don't reference the MCP tool name — Claude might try to re-call it.
// Just note what was done. The result follows as a TOOL RESULT message.
const argsStr = block.arguments
? JSON.stringify(block.arguments)
: "{}";
return `[Used ${block.name} tool with args: ${argsStr}]`;
const argsStr = args ? JSON.stringify(args) : "{}";
return `[Used ${name} tool with args: ${argsStr}]`;
}
const claudeName = mapPiToolNameToClaude(block.name);
const claudeArgs =
block.arguments && typeof block.arguments === "object"
? translatePiArgsToClaude(
block.name,
block.arguments as Record<string, unknown>,
)
: block.arguments;
const claudeName = mapPiToolNameToClaude(name);
const claudeArgs = args ? translatePiArgsToClaude(name, args) : undefined;
const argsStr = claudeArgs ? JSON.stringify(claudeArgs) : "{}";
return `Historical tool call (non-executable): ${claudeName} args=${argsStr}`;
}
// Unknown block types are represented as a placeholder
return `[${block.type}]`;
return `[${String(block.type)}]`;
})
.join("\n");
}
@@ -454,14 +464,15 @@ function contentToText(content: string | any[]): string {
* Handles string content and array of content blocks.
* Image blocks get placeholder text (actual image passthrough handled separately).
*/
function toolResultContentToText(content: string | any[]): string {
function toolResultContentToText(content: string | unknown[]): string {
if (typeof content === "string") return content;
if (!Array.isArray(content)) return "";
const texts: string[] = [];
for (const block of content) {
for (const rawBlock of content) {
const block = rawBlock as Record<string, unknown>;
if (block.type === "text") {
texts.push(block.text ?? "");
texts.push(typeof block.text === "string" ? block.text : "");
} else if (block.type === "image") {
texts.push("[An image was shared here but could not be included]");
placeholderImageCount++;
@@ -473,9 +484,9 @@ function toolResultContentToText(content: string | any[]): string {
/**
* Check if a tool result content array contains image blocks.
*/
function toolResultHasImages(content: string | any[]): boolean {
function toolResultHasImages(content: string | unknown[]): boolean {
if (typeof content === "string" || !Array.isArray(content)) return false;
return content.some((block) => block.type === "image");
return content.some((block) => (block as Record<string, unknown>).type === "image");
}
/**