@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 +21 -0
- package/README.md +69 -0
- package/extensions/finance.ts +6 -0
- package/package.json +46 -0
- package/src/client.ts +96 -0
- package/src/config.ts +39 -0
- package/src/index.ts +5 -0
- package/src/output.ts +38 -0
- package/src/tools.ts +226 -0
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)
|
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
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
|
+
}
|