ray-finance 0.2.0
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/.claude/settings.local.json +16 -0
- package/.env.example +13 -0
- package/.github/ISSUE_TEMPLATE/bug_report.md +19 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +9 -0
- package/.github/PULL_REQUEST_TEMPLATE.md +5 -0
- package/.github/workflows/ci.yml +21 -0
- package/CHANGELOG.md +16 -0
- package/CODE_OF_CONDUCT.md +31 -0
- package/CONTRIBUTING.md +41 -0
- package/Dockerfile +8 -0
- package/LICENSE +21 -0
- package/README.md +168 -0
- package/SECURITY.md +36 -0
- package/SPEC.md +374 -0
- package/dist/ai/agent.d.ts +2 -0
- package/dist/ai/agent.js +80 -0
- package/dist/ai/audit.d.ts +3 -0
- package/dist/ai/audit.js +6 -0
- package/dist/ai/context.d.ts +6 -0
- package/dist/ai/context.js +89 -0
- package/dist/ai/insights.d.ts +3 -0
- package/dist/ai/insights.js +378 -0
- package/dist/ai/memory.d.ts +14 -0
- package/dist/ai/memory.js +12 -0
- package/dist/ai/redactor.d.ts +2 -0
- package/dist/ai/redactor.js +92 -0
- package/dist/ai/system-prompt.d.ts +2 -0
- package/dist/ai/system-prompt.js +85 -0
- package/dist/ai/tools.d.ts +4 -0
- package/dist/ai/tools.js +695 -0
- package/dist/alerts/index.d.ts +11 -0
- package/dist/alerts/index.js +95 -0
- package/dist/auth/anthropic.d.ts +7 -0
- package/dist/auth/anthropic.js +85 -0
- package/dist/auth/pkce.d.ts +5 -0
- package/dist/auth/pkce.js +10 -0
- package/dist/auth/store.d.ts +12 -0
- package/dist/auth/store.js +51 -0
- package/dist/cli/backup.d.ts +2 -0
- package/dist/cli/backup.js +85 -0
- package/dist/cli/chat.d.ts +1 -0
- package/dist/cli/chat.js +97 -0
- package/dist/cli/commands.d.ts +13 -0
- package/dist/cli/commands.js +201 -0
- package/dist/cli/format.d.ts +12 -0
- package/dist/cli/format.js +119 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +176 -0
- package/dist/cli/scheduler.d.ts +2 -0
- package/dist/cli/scheduler.js +114 -0
- package/dist/cli/setup.d.ts +1 -0
- package/dist/cli/setup.js +168 -0
- package/dist/config.d.ts +22 -0
- package/dist/config.js +60 -0
- package/dist/daily-sync.d.ts +7 -0
- package/dist/daily-sync.js +94 -0
- package/dist/db/connection.d.ts +5 -0
- package/dist/db/connection.js +37 -0
- package/dist/db/encryption.d.ts +3 -0
- package/dist/db/encryption.js +24 -0
- package/dist/db/helpers.d.ts +16 -0
- package/dist/db/helpers.js +45 -0
- package/dist/db/schema.d.ts +2 -0
- package/dist/db/schema.js +194 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/plaid/client.d.ts +2 -0
- package/dist/plaid/client.js +22 -0
- package/dist/plaid/link.d.ts +8 -0
- package/dist/plaid/link.js +23 -0
- package/dist/plaid/sync.d.ts +18 -0
- package/dist/plaid/sync.js +186 -0
- package/dist/public/link.html +161 -0
- package/dist/queries/index.d.ts +163 -0
- package/dist/queries/index.js +411 -0
- package/dist/scoring/index.d.ts +53 -0
- package/dist/scoring/index.js +375 -0
- package/dist/server.d.ts +7 -0
- package/dist/server.js +140 -0
- package/docker-compose.yml +9 -0
- package/package.json +55 -0
- package/site/next-env.d.ts +6 -0
- package/site/next.config.ts +7 -0
- package/site/package-lock.json +1661 -0
- package/site/package.json +24 -0
- package/site/postcss.config.mjs +7 -0
- package/site/public/favicon.png +0 -0
- package/site/public/ray-og.jpg +0 -0
- package/site/public/robots.txt +4 -0
- package/site/public/sitemap.xml +8 -0
- package/site/src/app/copy-command.tsx +30 -0
- package/site/src/app/globals.css +87 -0
- package/site/src/app/layout.tsx +64 -0
- package/site/src/app/page.tsx +841 -0
- package/site/src/app/pii-scramble.tsx +190 -0
- package/site/src/app/reveal.tsx +29 -0
- package/site/tsconfig.json +21 -0
- package/src/ai/agent.ts +106 -0
- package/src/ai/audit.ts +11 -0
- package/src/ai/context.ts +93 -0
- package/src/ai/insights.ts +474 -0
- package/src/ai/memory.ts +21 -0
- package/src/ai/redactor.ts +102 -0
- package/src/ai/system-prompt.ts +90 -0
- package/src/ai/tools.ts +716 -0
- package/src/alerts/index.ts +123 -0
- package/src/cli/backup.ts +113 -0
- package/src/cli/chat.ts +105 -0
- package/src/cli/commands.ts +240 -0
- package/src/cli/format.ts +149 -0
- package/src/cli/index.ts +193 -0
- package/src/cli/scheduler.ts +116 -0
- package/src/cli/setup.ts +189 -0
- package/src/config.ts +81 -0
- package/src/daily-sync.ts +155 -0
- package/src/db/connection.ts +38 -0
- package/src/db/encryption.ts +29 -0
- package/src/db/helpers.ts +47 -0
- package/src/db/schema.ts +196 -0
- package/src/index.ts +3 -0
- package/src/plaid/client.ts +25 -0
- package/src/plaid/link.ts +25 -0
- package/src/plaid/sync.ts +219 -0
- package/src/public/link.html +161 -0
- package/src/queries/index.ts +586 -0
- package/src/scoring/index.ts +468 -0
- package/src/server.ts +162 -0
- package/tsconfig.json +16 -0
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useRef, useState, useCallback } from "react";
|
|
4
|
+
|
|
5
|
+
const CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789@#$%&*";
|
|
6
|
+
|
|
7
|
+
interface Segment {
|
|
8
|
+
text: string;
|
|
9
|
+
redacted?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const LINES: Segment[][] = [
|
|
13
|
+
[
|
|
14
|
+
{ text: "Sarah Chen", redacted: "[USER]" },
|
|
15
|
+
{ text: " earns $85k at " },
|
|
16
|
+
{ text: "Acme Corp", redacted: "[EMPLOYER]" },
|
|
17
|
+
{ text: "." },
|
|
18
|
+
],
|
|
19
|
+
[
|
|
20
|
+
{ text: "James", redacted: "[PARTNER]" },
|
|
21
|
+
{ text: " manages the household budget." },
|
|
22
|
+
],
|
|
23
|
+
[{ text: "Checking balance: $4,802" }],
|
|
24
|
+
[{ text: "Visa balance: -$1,200 @ 22.99% APR" }],
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
// Collect all redactable segments in order
|
|
28
|
+
const REDACTABLE: { lineIdx: number; segIdx: number }[] = [];
|
|
29
|
+
LINES.forEach((line, lineIdx) => {
|
|
30
|
+
line.forEach((seg, segIdx) => {
|
|
31
|
+
if (seg.redacted) REDACTABLE.push({ lineIdx, segIdx });
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
function scrambleText(original: string, target: string, progress: number): string {
|
|
36
|
+
const len = Math.max(original.length, target.length);
|
|
37
|
+
let result = "";
|
|
38
|
+
for (let i = 0; i < len; i++) {
|
|
39
|
+
const charProgress = Math.min(1, Math.max(0, progress * len - i) / 3);
|
|
40
|
+
if (charProgress >= 1) {
|
|
41
|
+
result += target[i] ?? "";
|
|
42
|
+
} else if (charProgress > 0) {
|
|
43
|
+
result += CHARS[Math.floor(Math.random() * CHARS.length)];
|
|
44
|
+
} else {
|
|
45
|
+
result += original[i] ?? "";
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return result;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function useInView(ref: React.RefObject<HTMLElement | null>, threshold = 0.5) {
|
|
52
|
+
const [inView, setInView] = useState(false);
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
const el = ref.current;
|
|
55
|
+
if (!el) return;
|
|
56
|
+
const observer = new IntersectionObserver(
|
|
57
|
+
([entry]) => {
|
|
58
|
+
if (entry.isIntersecting) {
|
|
59
|
+
setInView(true);
|
|
60
|
+
observer.disconnect();
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
{ threshold }
|
|
64
|
+
);
|
|
65
|
+
observer.observe(el);
|
|
66
|
+
return () => observer.disconnect();
|
|
67
|
+
}, [ref, threshold]);
|
|
68
|
+
return inView;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Which word index is currently scrambling (-1 = not started, >= REDACTABLE.length = done)
|
|
72
|
+
type AnimState = {
|
|
73
|
+
/** Index into REDACTABLE of the word currently animating */
|
|
74
|
+
activeWord: number;
|
|
75
|
+
/** 0..1 progress of the current word's scramble */
|
|
76
|
+
wordProgress: number;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
export function PIIScramble() {
|
|
80
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
81
|
+
const inView = useInView(containerRef);
|
|
82
|
+
const [anim, setAnim] = useState<AnimState>({ activeWord: -1, wordProgress: 0 });
|
|
83
|
+
|
|
84
|
+
const done = anim.activeWord >= REDACTABLE.length;
|
|
85
|
+
const scrambling = anim.activeWord >= 0 && !done;
|
|
86
|
+
|
|
87
|
+
const runWord = useCallback((wordIdx: number) => {
|
|
88
|
+
const SCRAMBLE_DURATION = 800; // ms per word
|
|
89
|
+
const start = performance.now();
|
|
90
|
+
|
|
91
|
+
function tick() {
|
|
92
|
+
const elapsed = performance.now() - start;
|
|
93
|
+
const p = Math.min(1, elapsed / SCRAMBLE_DURATION);
|
|
94
|
+
setAnim({ activeWord: wordIdx, wordProgress: p });
|
|
95
|
+
if (p < 1) {
|
|
96
|
+
requestAnimationFrame(tick);
|
|
97
|
+
} else if (wordIdx + 1 < REDACTABLE.length) {
|
|
98
|
+
// Pause 500ms then start next word
|
|
99
|
+
setTimeout(() => runWord(wordIdx + 1), 500);
|
|
100
|
+
} else {
|
|
101
|
+
// All done
|
|
102
|
+
setAnim({ activeWord: REDACTABLE.length, wordProgress: 1 });
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
requestAnimationFrame(tick);
|
|
106
|
+
}, []);
|
|
107
|
+
|
|
108
|
+
useEffect(() => {
|
|
109
|
+
if (!inView) return;
|
|
110
|
+
// Initial delay before first word starts
|
|
111
|
+
const delay = setTimeout(() => runWord(0), 800);
|
|
112
|
+
return () => clearTimeout(delay);
|
|
113
|
+
}, [inView, runWord]);
|
|
114
|
+
|
|
115
|
+
function getSegmentDisplay(lineIdx: number, segIdx: number, seg: Segment): { text: string; color: string } {
|
|
116
|
+
if (!seg.redacted) {
|
|
117
|
+
return { text: seg.text, color: "text-stone-300" };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Find this segment's index in the redactable list
|
|
121
|
+
const wordIdx = REDACTABLE.findIndex(r => r.lineIdx === lineIdx && r.segIdx === segIdx);
|
|
122
|
+
|
|
123
|
+
if (anim.activeWord < 0 || wordIdx > anim.activeWord) {
|
|
124
|
+
// Not yet reached — show original
|
|
125
|
+
return { text: seg.text, color: "text-red-400" };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (wordIdx < anim.activeWord || done) {
|
|
129
|
+
// Already redacted
|
|
130
|
+
return { text: seg.redacted!, color: "text-lime-400" };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Currently scrambling this word
|
|
134
|
+
return {
|
|
135
|
+
text: scrambleText(seg.text, seg.redacted!, anim.wordProgress),
|
|
136
|
+
color: "text-amber-300",
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const statusLabel = done
|
|
141
|
+
? "PII redacted before sending to AI"
|
|
142
|
+
: scrambling
|
|
143
|
+
? "Redacting..."
|
|
144
|
+
: "Raw financial data";
|
|
145
|
+
|
|
146
|
+
return (
|
|
147
|
+
<div ref={containerRef} className="rounded-xl border border-stone-200 bg-white p-6 sm:p-8">
|
|
148
|
+
<div className="mb-6 flex items-center gap-3">
|
|
149
|
+
<div
|
|
150
|
+
className={`h-2 w-2 rounded-full transition-colors duration-300 ${
|
|
151
|
+
done
|
|
152
|
+
? "bg-lime-400"
|
|
153
|
+
: scrambling
|
|
154
|
+
? "bg-amber-400 animate-pulse"
|
|
155
|
+
: "bg-stone-300"
|
|
156
|
+
}`}
|
|
157
|
+
/>
|
|
158
|
+
<p className="font-mono text-xs tracking-wide text-stone-400 uppercase">
|
|
159
|
+
{statusLabel}
|
|
160
|
+
</p>
|
|
161
|
+
</div>
|
|
162
|
+
|
|
163
|
+
<div className="rounded-lg bg-stone-950 p-4 sm:p-6 font-mono text-xs sm:text-sm leading-relaxed">
|
|
164
|
+
{LINES.map((segments, lineIdx) => (
|
|
165
|
+
<p key={lineIdx} className="text-stone-300">
|
|
166
|
+
{segments.map((seg, segIdx) => {
|
|
167
|
+
const { text, color } = getSegmentDisplay(lineIdx, segIdx, seg);
|
|
168
|
+
return (
|
|
169
|
+
<span key={segIdx} className={color}>
|
|
170
|
+
{text}
|
|
171
|
+
</span>
|
|
172
|
+
);
|
|
173
|
+
})}
|
|
174
|
+
</p>
|
|
175
|
+
))}
|
|
176
|
+
</div>
|
|
177
|
+
|
|
178
|
+
<div className="mt-4 flex items-center gap-4 text-xs text-stone-400">
|
|
179
|
+
<span className="flex items-center gap-1.5">
|
|
180
|
+
<span className="inline-block h-2 w-2 rounded-full bg-red-400" />
|
|
181
|
+
PII in local database
|
|
182
|
+
</span>
|
|
183
|
+
<span className="flex items-center gap-1.5">
|
|
184
|
+
<span className="inline-block h-2 w-2 rounded-full bg-lime-400" />
|
|
185
|
+
What the AI receives
|
|
186
|
+
</span>
|
|
187
|
+
</div>
|
|
188
|
+
</div>
|
|
189
|
+
);
|
|
190
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useRef } from "react";
|
|
4
|
+
|
|
5
|
+
export function Reveal({ children, className = "" }: { children: React.ReactNode; className?: string }) {
|
|
6
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
7
|
+
|
|
8
|
+
useEffect(() => {
|
|
9
|
+
const el = ref.current;
|
|
10
|
+
if (!el) return;
|
|
11
|
+
const observer = new IntersectionObserver(
|
|
12
|
+
([entry]) => {
|
|
13
|
+
if (entry.isIntersecting) {
|
|
14
|
+
el.classList.add("visible");
|
|
15
|
+
observer.disconnect();
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
{ threshold: 0.15 }
|
|
19
|
+
);
|
|
20
|
+
observer.observe(el);
|
|
21
|
+
return () => observer.disconnect();
|
|
22
|
+
}, []);
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<div ref={ref} className={`reveal ${className}`}>
|
|
26
|
+
{children}
|
|
27
|
+
</div>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2017",
|
|
4
|
+
"lib": ["dom", "dom.iterable", "esnext"],
|
|
5
|
+
"allowJs": true,
|
|
6
|
+
"skipLibCheck": true,
|
|
7
|
+
"strict": true,
|
|
8
|
+
"noEmit": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"module": "esnext",
|
|
11
|
+
"moduleResolution": "bundler",
|
|
12
|
+
"resolveJsonModule": true,
|
|
13
|
+
"isolatedModules": true,
|
|
14
|
+
"jsx": "preserve",
|
|
15
|
+
"incremental": true,
|
|
16
|
+
"plugins": [{ "name": "next" }],
|
|
17
|
+
"paths": { "@/*": ["./src/*"] }
|
|
18
|
+
},
|
|
19
|
+
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
|
20
|
+
"exclude": ["node_modules"]
|
|
21
|
+
}
|
package/src/ai/agent.ts
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import Anthropic from "@anthropic-ai/sdk";
|
|
2
|
+
import type Database from "better-sqlite3-multiple-ciphers";
|
|
3
|
+
import { config, useManaged, RAY_PROXY_BASE } from "../config.js";
|
|
4
|
+
import { buildSystemPrompt } from "./system-prompt.js";
|
|
5
|
+
import { toolDefinitions, executeTool } from "./tools.js";
|
|
6
|
+
import { getConversationHistory, saveMessage } from "./memory.js";
|
|
7
|
+
import { logToolCall } from "./audit.js";
|
|
8
|
+
import { redact, unredact } from "./redactor.js";
|
|
9
|
+
|
|
10
|
+
const anthropic = new Anthropic(
|
|
11
|
+
useManaged()
|
|
12
|
+
? { apiKey: config.rayApiKey, baseURL: `${RAY_PROXY_BASE}/ai` }
|
|
13
|
+
: { apiKey: config.anthropicKey }
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
function supportsThinking(model: string): boolean {
|
|
17
|
+
return /sonnet-4|opus-4/i.test(model);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function handleMessage(
|
|
21
|
+
db: Database.Database,
|
|
22
|
+
userMessage: string
|
|
23
|
+
): Promise<string> {
|
|
24
|
+
// Save incoming message
|
|
25
|
+
saveMessage(db, "user", userMessage);
|
|
26
|
+
|
|
27
|
+
// Load conversation context
|
|
28
|
+
const history = getConversationHistory(db, 20);
|
|
29
|
+
|
|
30
|
+
// Build system prompt and redact PII before sending to API
|
|
31
|
+
const systemPrompt = redact(buildSystemPrompt(db));
|
|
32
|
+
|
|
33
|
+
// Build messages array from history, redacting PII
|
|
34
|
+
const messages: Anthropic.MessageParam[] = history.map(h => ({
|
|
35
|
+
role: h.role as "user" | "assistant",
|
|
36
|
+
content: redact(h.content),
|
|
37
|
+
}));
|
|
38
|
+
|
|
39
|
+
// Ensure last message is the current user message
|
|
40
|
+
if (messages.length === 0 || messages[messages.length - 1].content !== userMessage) {
|
|
41
|
+
messages.push({ role: "user", content: redact(userMessage) });
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Extended thinking config
|
|
45
|
+
const useThinking = config.thinkingBudget > 0 && supportsThinking(config.model);
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
// Build API params
|
|
49
|
+
const apiParams: any = {
|
|
50
|
+
model: config.model,
|
|
51
|
+
max_tokens: useThinking ? 16000 : 4096,
|
|
52
|
+
system: systemPrompt,
|
|
53
|
+
tools: toolDefinitions,
|
|
54
|
+
messages,
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
if (useThinking) {
|
|
58
|
+
apiParams.thinking = {
|
|
59
|
+
type: "enabled",
|
|
60
|
+
budget_tokens: config.thinkingBudget,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Initial API call
|
|
65
|
+
let response = await anthropic.messages.create(apiParams);
|
|
66
|
+
|
|
67
|
+
// Agentic tool loop
|
|
68
|
+
while (response.stop_reason === "tool_use") {
|
|
69
|
+
// Filter out thinking blocks before adding to messages
|
|
70
|
+
const assistantContent = response.content.filter(
|
|
71
|
+
(b: any) => b.type !== "thinking"
|
|
72
|
+
) as Anthropic.ContentBlock[];
|
|
73
|
+
messages.push({ role: "assistant", content: assistantContent });
|
|
74
|
+
|
|
75
|
+
const toolResults: Anthropic.ToolResultBlockParam[] = [];
|
|
76
|
+
|
|
77
|
+
for (const block of assistantContent) {
|
|
78
|
+
if (block.type === "tool_use") {
|
|
79
|
+
const result = await executeTool(db, block.name, block.input);
|
|
80
|
+
logToolCall(db, block.name, block.input, result, response.usage?.output_tokens);
|
|
81
|
+
toolResults.push({
|
|
82
|
+
type: "tool_result",
|
|
83
|
+
tool_use_id: block.id,
|
|
84
|
+
content: redact(result),
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
messages.push({ role: "user", content: toolResults });
|
|
90
|
+
|
|
91
|
+
response = await anthropic.messages.create(apiParams);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Extract text response (filter out thinking blocks), restore PII for display
|
|
95
|
+
const textBlocks = response.content.filter((b: any) => b.type === "text");
|
|
96
|
+
const responseText = unredact(textBlocks.map((b: any) => b.text).join("\n"));
|
|
97
|
+
|
|
98
|
+
// Save assistant response
|
|
99
|
+
saveMessage(db, "assistant", responseText);
|
|
100
|
+
|
|
101
|
+
return responseText || "I looked into that but couldn't formulate a response. Could you try rephrasing?";
|
|
102
|
+
} catch (error: any) {
|
|
103
|
+
console.error("AI agent error:", error.message);
|
|
104
|
+
return "Sorry, I had trouble processing that. Could you try again?";
|
|
105
|
+
}
|
|
106
|
+
}
|
package/src/ai/audit.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type Database from "better-sqlite3-multiple-ciphers";
|
|
2
|
+
|
|
3
|
+
export function logToolCall(db: Database.Database, toolName: string, inputParams: any, resultSummary: string, tokensUsed?: number): void {
|
|
4
|
+
db.prepare(
|
|
5
|
+
`INSERT INTO ai_audit_log (tool_name, input_params, result_summary, tokens_used) VALUES (?, ?, ?, ?)`
|
|
6
|
+
).run(toolName, JSON.stringify(inputParams), resultSummary.slice(0, 500), tokensUsed || null);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function getAuditLog(db: Database.Database, limit = 50): any[] {
|
|
10
|
+
return db.prepare(`SELECT * FROM ai_audit_log ORDER BY id DESC LIMIT ?`).all(limit);
|
|
11
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
|
|
2
|
+
import { resolve } from "path";
|
|
3
|
+
import { homedir } from "os";
|
|
4
|
+
|
|
5
|
+
const CONTEXT_PATH = resolve(homedir(), ".ray", "context.md");
|
|
6
|
+
|
|
7
|
+
export function getContextPath(): string {
|
|
8
|
+
return CONTEXT_PATH;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function readContext(): string {
|
|
12
|
+
if (!existsSync(CONTEXT_PATH)) return "";
|
|
13
|
+
try {
|
|
14
|
+
return readFileSync(CONTEXT_PATH, "utf-8");
|
|
15
|
+
} catch {
|
|
16
|
+
return "";
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function writeContext(content: string): void {
|
|
21
|
+
const dir = resolve(homedir(), ".ray");
|
|
22
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
23
|
+
writeFileSync(CONTEXT_PATH, content, "utf-8");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function isContextEmpty(): boolean {
|
|
27
|
+
const content = readContext();
|
|
28
|
+
if (!content || content.trim().length === 0) return true;
|
|
29
|
+
// Check if it still has placeholder text (template hasn't been filled)
|
|
30
|
+
const placeholders = ["(Add income sources", "(Linked accounts will appear", "(Add financial goals", "(Add current financial strategy", "(Log important financial decisions", "(Track action items"];
|
|
31
|
+
const filledSections = placeholders.filter(p => !content.includes(p)).length;
|
|
32
|
+
// Consider empty if most sections still have placeholder text
|
|
33
|
+
return filledSections < 2;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function replaceContextSection(section: string, content: string): void {
|
|
37
|
+
let current = readContext();
|
|
38
|
+
if (!current) {
|
|
39
|
+
writeContext(`## ${section}\n${content}\n`);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const sectionHeader = `## ${section}`;
|
|
44
|
+
const sectionIdx = current.indexOf(sectionHeader);
|
|
45
|
+
|
|
46
|
+
if (sectionIdx !== -1) {
|
|
47
|
+
// Find the next ## heading after this section
|
|
48
|
+
const afterHeader = sectionIdx + sectionHeader.length;
|
|
49
|
+
const nextSectionIdx = current.indexOf("\n## ", afterHeader);
|
|
50
|
+
const before = current.slice(0, afterHeader);
|
|
51
|
+
const after = nextSectionIdx !== -1 ? current.slice(nextSectionIdx) : "";
|
|
52
|
+
current = `${before}\n${content}\n${after}`;
|
|
53
|
+
} else {
|
|
54
|
+
// Insert before ## Open Items if it exists, otherwise append
|
|
55
|
+
const openItemsIdx = current.indexOf("## Open Items");
|
|
56
|
+
if (openItemsIdx !== -1) {
|
|
57
|
+
current = `${current.slice(0, openItemsIdx)}${sectionHeader}\n${content}\n\n${current.slice(openItemsIdx)}`;
|
|
58
|
+
} else {
|
|
59
|
+
current = `${current.trimEnd()}\n\n${sectionHeader}\n${content}\n`;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
writeContext(current);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function createContextTemplate(userName: string): void {
|
|
67
|
+
if (existsSync(CONTEXT_PATH)) return; // don't overwrite existing
|
|
68
|
+
|
|
69
|
+
const template = `# Financial Context for ${userName}
|
|
70
|
+
|
|
71
|
+
## Family
|
|
72
|
+
- ${userName}
|
|
73
|
+
|
|
74
|
+
## Income
|
|
75
|
+
- (Add income sources and amounts)
|
|
76
|
+
|
|
77
|
+
## Accounts
|
|
78
|
+
- (Linked accounts will appear after syncing)
|
|
79
|
+
|
|
80
|
+
## Goals
|
|
81
|
+
- (Add financial goals)
|
|
82
|
+
|
|
83
|
+
## Strategy
|
|
84
|
+
- (Add current financial strategy and priorities)
|
|
85
|
+
|
|
86
|
+
## Key Decisions
|
|
87
|
+
- (Log important financial decisions here)
|
|
88
|
+
|
|
89
|
+
## Open Items
|
|
90
|
+
- (Track action items and follow-ups)
|
|
91
|
+
`;
|
|
92
|
+
writeContext(template);
|
|
93
|
+
}
|