feat: TypeScript SDK (agentlens-sdk) and OpenCode plugin (opencode-agentlens)
- packages/sdk-ts: BatchTransport, TraceBuilder, models, decision helpers Zero external deps, native fetch, ESM+CJS output - packages/opencode-plugin: OpenCode plugin with hooks for: - Session lifecycle (create/idle/error/delete/diff) - Tool execution capture (before/after -> TOOL_CALL spans + TOOL_SELECTION decisions) - LLM call tracking (chat.message -> LLM_CALL spans with model/provider) - Permission flow (permission.ask -> ESCALATION decisions) - File edit events - Model cost estimation (Claude, GPT-4o, o3-mini pricing)
This commit is contained in:
97
packages/opencode-plugin/src/utils.ts
Normal file
97
packages/opencode-plugin/src/utils.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
import type { JsonValue } from "agentlens-sdk";
|
||||
|
||||
export function truncate(str: string, maxLength: number): string {
|
||||
if (str.length <= maxLength) return str;
|
||||
return str.slice(0, maxLength) + "... [truncated]";
|
||||
}
|
||||
|
||||
export function extractToolMetadata(
|
||||
tool: string,
|
||||
args: unknown,
|
||||
): Record<string, unknown> {
|
||||
const a = args as Record<string, unknown> | null | undefined;
|
||||
if (!a || typeof a !== "object") return {};
|
||||
|
||||
switch (tool) {
|
||||
case "read":
|
||||
case "mcp_read":
|
||||
return { filePath: a["filePath"] };
|
||||
|
||||
case "write":
|
||||
case "mcp_write":
|
||||
return { filePath: a["filePath"] };
|
||||
|
||||
case "edit":
|
||||
case "mcp_edit":
|
||||
return { filePath: a["filePath"] };
|
||||
|
||||
case "bash":
|
||||
case "mcp_bash":
|
||||
return {
|
||||
command: truncate(String(a["command"] ?? ""), 200),
|
||||
};
|
||||
|
||||
case "glob":
|
||||
case "mcp_glob":
|
||||
return { pattern: a["pattern"] };
|
||||
|
||||
case "grep":
|
||||
case "mcp_grep":
|
||||
return { pattern: a["pattern"], path: a["path"] };
|
||||
|
||||
case "task":
|
||||
case "mcp_task":
|
||||
return {
|
||||
category: a["category"],
|
||||
description: a["description"],
|
||||
};
|
||||
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
const MODEL_COSTS: Record<string, { input: number; output: number }> = {
|
||||
"claude-opus-4-20250514": { input: 15, output: 75 },
|
||||
"claude-sonnet-4-20250514": { input: 3, output: 15 },
|
||||
"claude-haiku-3-20250307": { input: 0.25, output: 1.25 },
|
||||
"gpt-4o": { input: 2.5, output: 10 },
|
||||
"gpt-4o-mini": { input: 0.15, output: 0.6 },
|
||||
"gpt-4-turbo": { input: 10, output: 30 },
|
||||
"o3-mini": { input: 1.1, output: 4.4 },
|
||||
};
|
||||
|
||||
export function getModelCost(
|
||||
modelId: string,
|
||||
): { input: number; output: number } | undefined {
|
||||
const direct = MODEL_COSTS[modelId];
|
||||
if (direct) return direct;
|
||||
|
||||
for (const [key, cost] of Object.entries(MODEL_COSTS)) {
|
||||
if (modelId.includes(key)) return cost;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/** Coerce arbitrary values into SDK-compatible `JsonValue`, stringifying unknowns. */
|
||||
export function safeJsonValue(value: unknown): JsonValue {
|
||||
if (value === null || value === undefined) return null;
|
||||
if (
|
||||
typeof value === "string" ||
|
||||
typeof value === "number" ||
|
||||
typeof value === "boolean"
|
||||
) {
|
||||
return value;
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
return value.map((v) => safeJsonValue(v));
|
||||
}
|
||||
if (typeof value === "object") {
|
||||
const result: Record<string, JsonValue> = {};
|
||||
for (const [k, v] of Object.entries(value as Record<string, unknown>)) {
|
||||
result[k] = safeJsonValue(v);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
return String(value);
|
||||
}
|
||||
Reference in New Issue
Block a user