222 lines
5.8 KiB
TypeScript
222 lines
5.8 KiB
TypeScript
export type ToolDisplayActionSpec = {
|
|
label?: string;
|
|
detailKeys?: string[];
|
|
};
|
|
|
|
export type ToolDisplaySpec = {
|
|
title?: string;
|
|
label?: string;
|
|
detailKeys?: string[];
|
|
actions?: Record<string, ToolDisplayActionSpec>;
|
|
};
|
|
|
|
export type CoerceDisplayValueOptions = {
|
|
includeFalse?: boolean;
|
|
includeZero?: boolean;
|
|
includeNonFinite?: boolean;
|
|
maxStringChars?: number;
|
|
maxArrayEntries?: number;
|
|
};
|
|
|
|
export function normalizeToolName(name?: string): string {
|
|
return (name ?? "tool").trim();
|
|
}
|
|
|
|
export function defaultTitle(name: string): string {
|
|
const cleaned = name.replace(/_/g, " ").trim();
|
|
if (!cleaned) {
|
|
return "Tool";
|
|
}
|
|
return cleaned
|
|
.split(/\s+/)
|
|
.map((part) =>
|
|
part.length <= 2 && part.toUpperCase() === part
|
|
? part
|
|
: `${part.at(0)?.toUpperCase() ?? ""}${part.slice(1)}`,
|
|
)
|
|
.join(" ");
|
|
}
|
|
|
|
export function normalizeVerb(value?: string): string | undefined {
|
|
const trimmed = value?.trim();
|
|
if (!trimmed) {
|
|
return undefined;
|
|
}
|
|
return trimmed.replace(/_/g, " ");
|
|
}
|
|
|
|
export function coerceDisplayValue(
|
|
value: unknown,
|
|
opts: CoerceDisplayValueOptions = {},
|
|
): string | undefined {
|
|
const maxStringChars = opts.maxStringChars ?? 160;
|
|
const maxArrayEntries = opts.maxArrayEntries ?? 3;
|
|
|
|
if (value === null || value === undefined) {
|
|
return undefined;
|
|
}
|
|
if (typeof value === "string") {
|
|
const trimmed = value.trim();
|
|
if (!trimmed) {
|
|
return undefined;
|
|
}
|
|
const firstLine = trimmed.split(/\r?\n/)[0]?.trim() ?? "";
|
|
if (!firstLine) {
|
|
return undefined;
|
|
}
|
|
if (firstLine.length > maxStringChars) {
|
|
return `${firstLine.slice(0, Math.max(0, maxStringChars - 3))}…`;
|
|
}
|
|
return firstLine;
|
|
}
|
|
if (typeof value === "boolean") {
|
|
if (!value && !opts.includeFalse) {
|
|
return undefined;
|
|
}
|
|
return value ? "true" : "false";
|
|
}
|
|
if (typeof value === "number") {
|
|
if (!Number.isFinite(value)) {
|
|
return opts.includeNonFinite ? String(value) : undefined;
|
|
}
|
|
if (value === 0 && !opts.includeZero) {
|
|
return undefined;
|
|
}
|
|
return String(value);
|
|
}
|
|
if (Array.isArray(value)) {
|
|
const values = value
|
|
.map((item) => coerceDisplayValue(item, opts))
|
|
.filter((item): item is string => Boolean(item));
|
|
if (values.length === 0) {
|
|
return undefined;
|
|
}
|
|
const preview = values.slice(0, maxArrayEntries).join(", ");
|
|
return values.length > maxArrayEntries ? `${preview}…` : preview;
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
export function lookupValueByPath(args: unknown, path: string): unknown {
|
|
if (!args || typeof args !== "object") {
|
|
return undefined;
|
|
}
|
|
let current: unknown = args;
|
|
for (const segment of path.split(".")) {
|
|
if (!segment) {
|
|
return undefined;
|
|
}
|
|
if (!current || typeof current !== "object") {
|
|
return undefined;
|
|
}
|
|
const record = current as Record<string, unknown>;
|
|
current = record[segment];
|
|
}
|
|
return current;
|
|
}
|
|
|
|
export function formatDetailKey(raw: string, overrides: Record<string, string> = {}): string {
|
|
const segments = raw.split(".").filter(Boolean);
|
|
const last = segments.at(-1) ?? raw;
|
|
const override = overrides[last];
|
|
if (override) {
|
|
return override;
|
|
}
|
|
const cleaned = last.replace(/_/g, " ").replace(/-/g, " ");
|
|
const spaced = cleaned.replace(/([a-z0-9])([A-Z])/g, "$1 $2");
|
|
return spaced.trim().toLowerCase() || last.toLowerCase();
|
|
}
|
|
|
|
export function resolveReadDetail(args: unknown): string | undefined {
|
|
if (!args || typeof args !== "object") {
|
|
return undefined;
|
|
}
|
|
const record = args as Record<string, unknown>;
|
|
const path = typeof record.path === "string" ? record.path : undefined;
|
|
if (!path) {
|
|
return undefined;
|
|
}
|
|
const offset = typeof record.offset === "number" ? record.offset : undefined;
|
|
const limit = typeof record.limit === "number" ? record.limit : undefined;
|
|
if (offset !== undefined && limit !== undefined) {
|
|
return `${path}:${offset}-${offset + limit}`;
|
|
}
|
|
return path;
|
|
}
|
|
|
|
export function resolveWriteDetail(args: unknown): string | undefined {
|
|
if (!args || typeof args !== "object") {
|
|
return undefined;
|
|
}
|
|
const record = args as Record<string, unknown>;
|
|
const path = typeof record.path === "string" ? record.path : undefined;
|
|
return path;
|
|
}
|
|
|
|
export function resolveActionSpec(
|
|
spec: ToolDisplaySpec | undefined,
|
|
action: string | undefined,
|
|
): ToolDisplayActionSpec | undefined {
|
|
if (!spec || !action) {
|
|
return undefined;
|
|
}
|
|
return spec.actions?.[action] ?? undefined;
|
|
}
|
|
|
|
export function resolveDetailFromKeys(
|
|
args: unknown,
|
|
keys: string[],
|
|
opts: {
|
|
mode: "first" | "summary";
|
|
coerce?: CoerceDisplayValueOptions;
|
|
maxEntries?: number;
|
|
formatKey?: (raw: string) => string;
|
|
},
|
|
): string | undefined {
|
|
if (opts.mode === "first") {
|
|
for (const key of keys) {
|
|
const value = lookupValueByPath(args, key);
|
|
const display = coerceDisplayValue(value, opts.coerce);
|
|
if (display) {
|
|
return display;
|
|
}
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
const entries: Array<{ label: string; value: string }> = [];
|
|
for (const key of keys) {
|
|
const value = lookupValueByPath(args, key);
|
|
const display = coerceDisplayValue(value, opts.coerce);
|
|
if (!display) {
|
|
continue;
|
|
}
|
|
entries.push({ label: opts.formatKey ? opts.formatKey(key) : key, value: display });
|
|
}
|
|
if (entries.length === 0) {
|
|
return undefined;
|
|
}
|
|
if (entries.length === 1) {
|
|
return entries[0].value;
|
|
}
|
|
|
|
const seen = new Set<string>();
|
|
const unique: Array<{ label: string; value: string }> = [];
|
|
for (const entry of entries) {
|
|
const token = `${entry.label}:${entry.value}`;
|
|
if (seen.has(token)) {
|
|
continue;
|
|
}
|
|
seen.add(token);
|
|
unique.push(entry);
|
|
}
|
|
if (unique.length === 0) {
|
|
return undefined;
|
|
}
|
|
|
|
return unique
|
|
.slice(0, opts.maxEntries ?? 8)
|
|
.map((entry) => `${entry.label} ${entry.value}`)
|
|
.join(" · ");
|
|
}
|