@pi-stef/finance 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Stefano Fiorini
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,69 @@
1
+ # @pi-stef/finance
2
+
3
+ Pi extension client for `@pi-stef/finance-api` — exposes portfolio state, drift, and deterministic suggestions to the pi agent.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pi install npm:@pi-stef/finance
9
+ ```
10
+
11
+ Or via catalog:
12
+ ```bash
13
+ /ct add npm:@pi-stef/finance
14
+ ```
15
+
16
+ ## Prerequisites
17
+
18
+ The `@pi-stef/finance-api` service must be running (Docker or native). See the [finance-api README](../finance-api/README.md) for setup.
19
+
20
+ ## Configuration
21
+
22
+ Set environment variables or create `~/.pi/sf/finance/config.json`:
23
+
24
+ ```json
25
+ {
26
+ "apiUrl": "http://127.0.0.1:7780",
27
+ "token": "your-bearer-token"
28
+ }
29
+ ```
30
+
31
+ Environment variables (prefix `SF_FINANCE_`):
32
+
33
+ | Variable | Default | Description |
34
+ |----------|---------|-------------|
35
+ | `SF_FINANCE_API_URL` | `http://127.0.0.1:7780` | Finance API URL |
36
+ | `SF_FINANCE_TOKEN` | (from `~/.pi/sf/finance/token`) | Bearer token for API auth |
37
+
38
+ ## Tools
39
+
40
+ | Tool | Description |
41
+ |------|-------------|
42
+ | `sf_fin_market_status` | Get current US market session |
43
+ | `sf_fin_get_holdings` | Get all account holdings |
44
+ | `sf_fin_get_net_worth` | Get total portfolio value |
45
+ | `sf_fin_get_drift` | Get allocation drift vs target |
46
+ | `sf_fin_get_allocation` | Get current asset allocation |
47
+ | `sf_fin_list_goals` | List investment goals |
48
+ | `sf_fin_set_target` | Create/update investment goal |
49
+ | `sf_fin_get_suggestions` | Get pending suggestions |
50
+ | `sf_fin_dismiss_suggestion` | Dismiss a suggestion |
51
+ | `sf_fin_sync_now` | Trigger immediate data sync |
52
+ | `sf_fin_import_file` | Import holdings from CSV/OFX |
53
+ | `sf_fin_history` | Get price history |
54
+
55
+ ## Usage
56
+
57
+ Ask the pi agent:
58
+ - "What's my current portfolio allocation?"
59
+ - "How far am I from my target allocation?"
60
+ - "What should I buy/sell to rebalance?"
61
+ - "Import my Fidelity positions from ~/Downloads/positions.csv"
62
+
63
+ ## Disclaimer
64
+
65
+ **This is not financial advice.** The suggestions are computed deterministically from your configured goals and current holdings. The LLM applies judgment but never recomputes the numbers. Always consult a qualified financial advisor before making investment decisions.
66
+
67
+ ## License
68
+
69
+ [MIT](../../LICENSE)
@@ -0,0 +1,6 @@
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import { registerFinanceTools } from "../src/index";
3
+
4
+ export default function financeExtension(pi: ExtensionAPI): void {
5
+ registerFinanceTools(pi);
6
+ }
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@pi-stef/finance",
3
+ "version": "0.1.1",
4
+ "description": "Pi extension client for @pi-stef/finance-api: exposes portfolio state, drift, and deterministic suggestions to the pi agent.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/sfiorini/pi-stef.git",
10
+ "directory": "packages/finance"
11
+ },
12
+ "homepage": "https://sfiorini.github.io/pi-stef/packages/finance.html",
13
+ "files": [
14
+ "src/",
15
+ "extensions/"
16
+ ],
17
+ "exports": {
18
+ ".": "./src/index.ts",
19
+ "./package.json": "./package.json"
20
+ },
21
+ "keywords": [
22
+ "pi-package",
23
+ "pi-extension",
24
+ "finance",
25
+ "portfolio",
26
+ "investing"
27
+ ],
28
+ "dependencies": {
29
+ "@pi-stef/paths": "^0.3.0",
30
+ "@pi-stef/finance-api": "^0.1.0",
31
+ "@sinclair/typebox": "*"
32
+ },
33
+ "peerDependencies": {
34
+ "@earendil-works/pi-ai": "*",
35
+ "@earendil-works/pi-coding-agent": "*"
36
+ },
37
+ "pi": {
38
+ "extensions": [
39
+ "./extensions"
40
+ ]
41
+ },
42
+ "scripts": {
43
+ "test": "vitest run",
44
+ "typecheck": "tsc --noEmit -p tsconfig.json"
45
+ }
46
+ }
package/src/client.ts ADDED
@@ -0,0 +1,96 @@
1
+ // HTTP client for finance-api service
2
+ // Maps operations to GET/POST methods per the M7 contract table
3
+
4
+ // Explicit op→path mapping matching server routes exactly
5
+ export const OP_PATH: Record<string, string> = {
6
+ market_status: "/v1/market-status",
7
+ get_holdings: "/v1/holdings",
8
+ get_net_worth: "/v1/net-worth",
9
+ get_drift: "/v1/drift",
10
+ get_allocation: "/v1/allocation",
11
+ list_goals: "/v1/goals",
12
+ set_target: "/v1/goals",
13
+ get_suggestions: "/v1/suggestions",
14
+ dismiss_suggestion: "/v1/suggestions/dismiss",
15
+ sync_now: "/v1/sync",
16
+ import_file: "/v1/import",
17
+ history: "/v1/history",
18
+ health: "/v1/health",
19
+ export: "/v1/export",
20
+ };
21
+
22
+ export const OP_METHOD: Record<string, "GET" | "POST"> = {
23
+ market_status: "GET",
24
+ get_holdings: "GET",
25
+ get_net_worth: "GET",
26
+ get_drift: "GET",
27
+ get_allocation: "GET",
28
+ list_goals: "GET",
29
+ set_target: "POST",
30
+ get_suggestions: "GET",
31
+ dismiss_suggestion: "POST",
32
+ sync_now: "POST",
33
+ import_file: "POST",
34
+ history: "GET",
35
+ health: "GET",
36
+ export: "POST",
37
+ };
38
+
39
+ export interface FinanceClientConfig {
40
+ apiUrl: string;
41
+ token: string;
42
+ }
43
+
44
+ export interface CallResult<T = unknown> {
45
+ ok: boolean;
46
+ data?: T;
47
+ error?: { code: string; message: string };
48
+ staleAt?: number | null;
49
+ staleReason?: string | null;
50
+ }
51
+
52
+ export function createFinanceClient(config: FinanceClientConfig) {
53
+ const { apiUrl, token } = config;
54
+
55
+ async function callOp<T = unknown>(op: string, params?: Record<string, unknown>): Promise<T> {
56
+ const method = OP_METHOD[op];
57
+ if (!method) throw new Error(`Unknown operation: ${op}`);
58
+ const path = OP_PATH[op];
59
+ if (!path) throw new Error(`No path mapping for operation: ${op}`);
60
+
61
+ const url = new URL(`${apiUrl}${path}`);
62
+ const headers: Record<string, string> = {
63
+ Authorization: `Bearer ${token}`,
64
+ "Content-Type": "application/json",
65
+ };
66
+
67
+ let body: string | undefined;
68
+ if (method === "GET" && params) {
69
+ for (const [key, value] of Object.entries(params)) {
70
+ if (value !== undefined && value !== null) {
71
+ url.searchParams.set(key, String(value));
72
+ }
73
+ }
74
+ } else if (method === "POST" && params) {
75
+ body = JSON.stringify(params);
76
+ }
77
+
78
+ let res: Response;
79
+ try {
80
+ res = await fetch(url.toString(), { method, headers, body });
81
+ } catch (err) {
82
+ throw new Error(`service_unavailable: ${err instanceof Error ? err.message : String(err)}`);
83
+ }
84
+
85
+ const json = await res.json() as CallResult<T>;
86
+ if (!json.ok) {
87
+ throw new Error(`${json.error?.code ?? "unknown"}: ${json.error?.message ?? "Unknown error"}`);
88
+ }
89
+
90
+ return json.data as T;
91
+ }
92
+
93
+ return { callOp };
94
+ }
95
+
96
+ export type FinanceClient = ReturnType<typeof createFinanceClient>;
package/src/config.ts ADDED
@@ -0,0 +1,39 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { globalConfig, globalDir } from "@pi-stef/paths";
4
+
5
+ export interface FinanceConfig {
6
+ apiUrl: string;
7
+ token: string;
8
+ }
9
+
10
+ export async function loadFinanceConfig(
11
+ env: Record<string, string | undefined> = process.env,
12
+ homeDir: string = process.env.HOME ?? process.cwd(),
13
+ ): Promise<FinanceConfig> {
14
+ let fileToken = "";
15
+ let fileUrl = "";
16
+ try {
17
+ const raw = JSON.parse(await readFile(globalConfig("finance", homeDir), "utf8")) as Partial<FinanceConfig>;
18
+ fileToken = raw.token ?? "";
19
+ fileUrl = raw.apiUrl ?? "";
20
+ } catch (e) {
21
+ if (!(e instanceof Error && "code" in e && (e as { code: string }).code === "ENOENT")) throw e;
22
+ }
23
+
24
+ // Try to read auto-generated token from ~/.pi/sf/finance/token
25
+ let autoToken = "";
26
+ if (!fileToken && !env.SF_FINANCE_TOKEN) {
27
+ try {
28
+ const tokenPath = path.join(globalDir("finance", homeDir), "token");
29
+ autoToken = (await readFile(tokenPath, "utf8")).trim();
30
+ } catch {
31
+ // Token file doesn't exist yet (service not started)
32
+ }
33
+ }
34
+
35
+ return {
36
+ apiUrl: env.SF_FINANCE_API_URL || fileUrl || "http://127.0.0.1:7780",
37
+ token: env.SF_FINANCE_TOKEN || fileToken || autoToken || "",
38
+ };
39
+ }
package/src/index.ts ADDED
@@ -0,0 +1,5 @@
1
+ // Finance extension exports
2
+ export { loadFinanceConfig } from "./config";
3
+ export { registerFinanceTools } from "./tools";
4
+ export { createFinanceClient, OP_METHOD } from "./client";
5
+ export type { FinanceClient, FinanceClientConfig } from "./client";
package/src/output.ts ADDED
@@ -0,0 +1,38 @@
1
+ // Format structured service JSON → agent-readable text
2
+
3
+ export function formatHoldings(data: { accounts: { id: string; name: string; holdings: { symbol: string; quantity: number; asset_class: string }[] }[] }): string {
4
+ const lines: string[] = [];
5
+ for (const account of data.accounts) {
6
+ lines.push(`Account: ${account.name} (${account.id})`);
7
+ for (const h of account.holdings) {
8
+ lines.push(` ${h.symbol}: ${h.quantity} shares (${h.asset_class})`);
9
+ }
10
+ }
11
+ return lines.join("\n") || "No holdings found";
12
+ }
13
+
14
+ export function formatDrift(data: { drift: { class: string; currentPct: number; targetPct: number; deltaPct: number }[] }): string {
15
+ const lines: string[] = ["Allocation Drift:"];
16
+ for (const d of data.drift) {
17
+ const status = d.deltaPct > 0.02 ? "⚠️ OVER" : d.deltaPct < -0.02 ? "⚠️ UNDER" : "✓";
18
+ lines.push(` ${d.class}: ${(d.currentPct * 100).toFixed(1)}% → target ${(d.targetPct * 100).toFixed(1)}% (${status})`);
19
+ }
20
+ return lines.join("\n");
21
+ }
22
+
23
+ export function formatSuggestions(data: { suggestions: { id: string; kind: string; payload: unknown }[] }): string {
24
+ if (data.suggestions.length === 0) return "No pending suggestions";
25
+ const lines: string[] = ["Pending Suggestions:"];
26
+ for (const s of data.suggestions) {
27
+ lines.push(` [${s.kind}] ${JSON.stringify(s.payload)}`);
28
+ }
29
+ return lines.join("\n");
30
+ }
31
+
32
+ export function formatGeneric(data: unknown): string {
33
+ if (typeof data === "string") return data;
34
+ if (typeof data === "object" && data !== null) {
35
+ return JSON.stringify(data, null, 2);
36
+ }
37
+ return String(data);
38
+ }
package/src/tools.ts ADDED
@@ -0,0 +1,226 @@
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import { loadFinanceConfig } from "./config";
3
+ import { createFinanceClient } from "./client";
4
+ import { formatHoldings, formatDrift, formatSuggestions, formatGeneric } from "./output";
5
+
6
+ const NEVER_RECOMPUTE_GUIDELINE = "These numbers are computed by the service. Never recompute prices, allocations, or drift yourself — cite the returned values verbatim. When recommending an instrument, justify it against the engine's gap.";
7
+
8
+ export function registerFinanceTools(pi: ExtensionAPI): void {
9
+ // Helper to get client
10
+ async function getClient() {
11
+ const config = await loadFinanceConfig();
12
+ return createFinanceClient({ apiUrl: config.apiUrl, token: config.token });
13
+ }
14
+
15
+ // Market Status
16
+ pi.registerTool({
17
+ name: "sf_fin_market_status",
18
+ label: "Market Status",
19
+ description: "Get current US market session (pre/intraday/post/closed)",
20
+ parameters: {},
21
+ promptSnippet: "Check if the US stock market is currently open.",
22
+ promptGuidelines: [NEVER_RECOMPUTE_GUIDELINE],
23
+ execute: async () => {
24
+ const client = await getClient();
25
+ const data = await client.callOp<{ session: string; timestamp: number }>("market_status");
26
+ return { content: [{ type: "text", text: `Market session: ${data.session}` }], details: { implemented: true } };
27
+ },
28
+ });
29
+
30
+ // Get Holdings
31
+ pi.registerTool({
32
+ name: "sf_fin_get_holdings",
33
+ label: "Get Holdings",
34
+ description: "Get all account holdings with quantities and asset classes",
35
+ parameters: {},
36
+ promptSnippet: "Retrieve current portfolio holdings across all accounts.",
37
+ promptGuidelines: [NEVER_RECOMPUTE_GUIDELINE],
38
+ execute: async () => {
39
+ const client = await getClient();
40
+ const data = await client.callOp("get_holdings");
41
+ return { content: [{ type: "text", text: formatHoldings(data as Parameters<typeof formatHoldings>[0]) }], details: { implemented: true } };
42
+ },
43
+ });
44
+
45
+ // Get Net Worth
46
+ pi.registerTool({
47
+ name: "sf_fin_get_net_worth",
48
+ label: "Get Net Worth",
49
+ description: "Get total portfolio value across all accounts",
50
+ parameters: {},
51
+ promptSnippet: "Calculate total portfolio value.",
52
+ promptGuidelines: [NEVER_RECOMPUTE_GUIDELINE],
53
+ execute: async () => {
54
+ const client = await getClient();
55
+ const data = await client.callOp<{ netWorth: number; accountCount: number }>("get_net_worth");
56
+ return { content: [{ type: "text", text: `Net Worth: $${data.netWorth.toLocaleString()} (${data.accountCount} accounts)` }], details: { implemented: true } };
57
+ },
58
+ });
59
+
60
+ // Get Drift
61
+ pi.registerTool({
62
+ name: "sf_fin_get_drift",
63
+ label: "Get Drift",
64
+ description: "Get allocation drift vs target",
65
+ parameters: {},
66
+ promptSnippet: "Check portfolio drift from target allocation.",
67
+ promptGuidelines: [NEVER_RECOMPUTE_GUIDELINE],
68
+ execute: async () => {
69
+ const client = await getClient();
70
+ const data = await client.callOp("get_drift");
71
+ return { content: [{ type: "text", text: formatDrift(data as Parameters<typeof formatDrift>[0]) }], details: { implemented: true } };
72
+ },
73
+ });
74
+
75
+ // Get Allocation
76
+ pi.registerTool({
77
+ name: "sf_fin_get_allocation",
78
+ label: "Get Allocation",
79
+ description: "Get current asset allocation by class",
80
+ parameters: {},
81
+ promptSnippet: "View current asset allocation breakdown.",
82
+ promptGuidelines: [NEVER_RECOMPUTE_GUIDELINE],
83
+ execute: async () => {
84
+ const client = await getClient();
85
+ const data = await client.callOp<{ allocation: Record<string, number>; totalValue: number }>("get_allocation");
86
+ const lines = Object.entries(data.allocation).map(([cls, pct]) => ` ${cls}: ${(pct * 100).toFixed(1)}%`);
87
+ return { content: [{ type: "text", text: `Asset Allocation (Total: $${data.totalValue.toLocaleString()}):\n${lines.join("\n")}` }], details: { implemented: true } };
88
+ },
89
+ });
90
+
91
+ // List Goals
92
+ pi.registerTool({
93
+ name: "sf_fin_list_goals",
94
+ label: "List Goals",
95
+ description: "List investment goals with target allocations",
96
+ parameters: {},
97
+ promptSnippet: "View configured investment goals.",
98
+ promptGuidelines: [NEVER_RECOMPUTE_GUIDELINE],
99
+ execute: async () => {
100
+ const client = await getClient();
101
+ const data = await client.callOp<{ goals: { id: string; name: string; targetAllocation: Record<string, number> }[] }>("list_goals");
102
+ if (data.goals.length === 0) return { content: [{ type: "text", text: "No goals configured" }], details: { implemented: true } };
103
+ const lines = data.goals.map((g) => `${g.name}: ${JSON.stringify(g.targetAllocation)}`);
104
+ return { content: [{ type: "text", text: `Investment Goals:\n${lines.join("\n")}` }], details: { implemented: true } };
105
+ },
106
+ });
107
+
108
+ // Set Target
109
+ pi.registerTool({
110
+ name: "sf_fin_set_target",
111
+ label: "Set Target",
112
+ description: "Create or update an investment goal",
113
+ parameters: {
114
+ type: "object",
115
+ properties: {
116
+ id: { type: "string", description: "Goal ID" },
117
+ name: { type: "string", description: "Goal name" },
118
+ targetAllocation: { type: "object", description: "Target allocation by asset class (must sum to ~1.0)" },
119
+ riskLimits: { type: "object", description: "Risk limits (e.g., maxSinglePosition, maxCashDrag)" },
120
+ horizonYears: { type: "number", description: "Investment horizon in years" },
121
+ },
122
+ required: ["id", "name", "targetAllocation"],
123
+ },
124
+ promptSnippet: "Create or update an investment goal with target allocation.",
125
+ promptGuidelines: [NEVER_RECOMPUTE_GUIDELINE],
126
+ execute: async (params) => {
127
+ const client = await getClient();
128
+ const data = await client.callOp<{ id: string }>("set_target", params as unknown as Record<string, unknown>);
129
+ return { content: [{ type: "text", text: `Goal ${data.id} saved` }], details: { implemented: true } };
130
+ },
131
+ });
132
+
133
+ // Get Suggestions
134
+ pi.registerTool({
135
+ name: "sf_fin_get_suggestions",
136
+ label: "Get Suggestions",
137
+ description: "Get pending investment suggestions from the quant engine",
138
+ parameters: {},
139
+ promptSnippet: "Retrieve deterministic suggestions from the quant engine.",
140
+ promptGuidelines: [NEVER_RECOMPUTE_GUIDELINE],
141
+ execute: async () => {
142
+ const client = await getClient();
143
+ const data = await client.callOp("get_suggestions");
144
+ return { content: [{ type: "text", text: formatSuggestions(data as Parameters<typeof formatSuggestions>[0]) }], details: { implemented: true } };
145
+ },
146
+ });
147
+
148
+ // Dismiss Suggestion
149
+ pi.registerTool({
150
+ name: "sf_fin_dismiss_suggestion",
151
+ label: "Dismiss Suggestion",
152
+ description: "Dismiss a pending suggestion",
153
+ parameters: {
154
+ type: "object",
155
+ properties: {
156
+ id: { type: "string", description: "Suggestion ID to dismiss" },
157
+ },
158
+ required: ["id"],
159
+ },
160
+ promptSnippet: "Dismiss a suggestion that's been addressed.",
161
+ promptGuidelines: [NEVER_RECOMPUTE_GUIDELINE],
162
+ execute: async (params) => {
163
+ const client = await getClient();
164
+ await client.callOp("dismiss_suggestion", params as unknown as Record<string, unknown>);
165
+ return { content: [{ type: "text", text: `Suggestion dismissed` }], details: { implemented: true } };
166
+ },
167
+ });
168
+
169
+ // Sync Now
170
+ pi.registerTool({
171
+ name: "sf_fin_sync_now",
172
+ label: "Sync Now",
173
+ description: "Trigger immediate data sync from all providers",
174
+ parameters: {},
175
+ promptSnippet: "Force an immediate sync of account data.",
176
+ promptGuidelines: [NEVER_RECOMPUTE_GUIDELINE],
177
+ execute: async () => {
178
+ const client = await getClient();
179
+ const data = await client.callOp<{ message: string }>("sync_now");
180
+ return { content: [{ type: "text", text: data.message }], details: { implemented: true } };
181
+ },
182
+ });
183
+
184
+ // Import File
185
+ pi.registerTool({
186
+ name: "sf_fin_import_file",
187
+ label: "Import File",
188
+ description: "Import holdings from a CSV/OFX file",
189
+ parameters: {
190
+ type: "object",
191
+ properties: {
192
+ filePath: { type: "string", description: "Path to CSV/OFX file" },
193
+ },
194
+ required: ["filePath"],
195
+ },
196
+ promptSnippet: "Import holdings from a file export.",
197
+ promptGuidelines: [NEVER_RECOMPUTE_GUIDELINE],
198
+ execute: async (params) => {
199
+ const client = await getClient();
200
+ const data = await client.callOp<{ message: string; filePath: string }>("import_file", params as unknown as Record<string, unknown>);
201
+ return { content: [{ type: "text", text: `${data.message}: ${data.filePath}` }], details: { implemented: true } };
202
+ },
203
+ });
204
+
205
+ // History
206
+ pi.registerTool({
207
+ name: "sf_fin_history",
208
+ label: "Price History",
209
+ description: "Get price history for a symbol",
210
+ parameters: {
211
+ type: "object",
212
+ properties: {
213
+ symbol: { type: "string", description: "Ticker symbol (e.g., AAPL, CRYPTO:BTC)" },
214
+ accountId: { type: "string", description: "Optional account ID filter" },
215
+ },
216
+ required: ["symbol"],
217
+ },
218
+ promptSnippet: "View price history for a security.",
219
+ promptGuidelines: [NEVER_RECOMPUTE_GUIDELINE],
220
+ execute: async (params) => {
221
+ const client = await getClient();
222
+ const data = await client.callOp("history", params as unknown as Record<string, unknown>);
223
+ return { content: [{ type: "text", text: formatGeneric(data) }], details: { implemented: true } };
224
+ },
225
+ });
226
+ }