ray-finance 0.4.1 → 0.4.2
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/dist/ai/agent.d.ts +5 -1
- package/dist/ai/agent.js +20 -1
- package/dist/ai/provider.d.ts +1 -0
- package/dist/ai/providers/anthropic.js +3 -1
- package/dist/ai/providers/openai-compat.js +2 -2
- package/dist/cli/chat.d.ts +4 -0
- package/dist/cli/chat.js +11 -427
- package/dist/cli/commands.js +1 -1
- package/dist/cli/ink/ChatApp.d.ts +8 -0
- package/dist/cli/ink/ChatApp.js +96 -0
- package/dist/cli/ink/PromptFrame.d.ts +10 -0
- package/dist/cli/ink/PromptFrame.js +11 -0
- package/dist/cli/ink/TextInput.d.ts +13 -0
- package/dist/cli/ink/TextInput.js +24 -0
- package/dist/cli/ink/hooks/useAgent.d.ts +27 -0
- package/dist/cli/ink/hooks/useAgent.js +77 -0
- package/dist/cli/ink/hooks/useBackgroundSync.d.ts +3 -0
- package/dist/cli/ink/hooks/useBackgroundSync.js +31 -0
- package/dist/cli/ink/hooks/useCtrlCExit.d.ts +16 -0
- package/dist/cli/ink/hooks/useCtrlCExit.js +43 -0
- package/dist/cli/ink/hooks/useFooterText.d.ts +3 -0
- package/dist/cli/ink/hooks/useFooterText.js +47 -0
- package/dist/cli/ink/hooks/useTextInput.d.ts +32 -0
- package/dist/cli/ink/hooks/useTextInput.js +356 -0
- package/dist/cli/ink/messages/AssistantMessage.d.ts +3 -0
- package/dist/cli/ink/messages/AssistantMessage.js +6 -0
- package/dist/cli/ink/messages/ErrorMessage.d.ts +4 -0
- package/dist/cli/ink/messages/ErrorMessage.js +6 -0
- package/dist/cli/ink/messages/InterruptedMessage.d.ts +1 -0
- package/dist/cli/ink/messages/InterruptedMessage.js +6 -0
- package/dist/cli/ink/messages/ThinkingLine.d.ts +12 -0
- package/dist/cli/ink/messages/ThinkingLine.js +23 -0
- package/dist/cli/ink/messages/UserMessage.d.ts +4 -0
- package/dist/cli/ink/messages/UserMessage.js +15 -0
- package/dist/cli/ink/mount.d.ts +6 -0
- package/dist/cli/ink/mount.js +12 -0
- package/dist/daily-sync.d.ts +6 -1
- package/dist/daily-sync.js +25 -24
- package/dist/queries/index.d.ts +2 -0
- package/dist/queries/index.js +14 -5
- package/package.json +5 -1
package/dist/ai/agent.d.ts
CHANGED
|
@@ -7,4 +7,8 @@ export type ProgressCallback = (event: {
|
|
|
7
7
|
toolCount: number;
|
|
8
8
|
elapsedMs: number;
|
|
9
9
|
}) => void;
|
|
10
|
-
|
|
10
|
+
/** Thrown by handleMessage when the caller aborts via AbortSignal */
|
|
11
|
+
export declare class AbortedError extends Error {
|
|
12
|
+
constructor();
|
|
13
|
+
}
|
|
14
|
+
export declare function handleMessage(db: Database.Database, userMessage: string, onProgress?: ProgressCallback, signal?: AbortSignal): Promise<string>;
|
package/dist/ai/agent.js
CHANGED
|
@@ -26,7 +26,14 @@ export const TOOL_LABELS = {
|
|
|
26
26
|
save_memory: "Remembering that",
|
|
27
27
|
update_context: "Updating your profile",
|
|
28
28
|
};
|
|
29
|
-
|
|
29
|
+
/** Thrown by handleMessage when the caller aborts via AbortSignal */
|
|
30
|
+
export class AbortedError extends Error {
|
|
31
|
+
constructor() {
|
|
32
|
+
super("aborted");
|
|
33
|
+
this.name = "AbortedError";
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
export async function handleMessage(db, userMessage, onProgress, signal) {
|
|
30
37
|
// Save incoming message
|
|
31
38
|
saveMessage(db, "user", userMessage);
|
|
32
39
|
// Load conversation context, truncated to fit token budget
|
|
@@ -55,7 +62,12 @@ export async function handleMessage(db, userMessage, onProgress) {
|
|
|
55
62
|
const useThinking = config.thinkingBudget > 0
|
|
56
63
|
&& provider.supportsThinking
|
|
57
64
|
&& supportsThinking(config.model);
|
|
65
|
+
const throwIfAborted = () => {
|
|
66
|
+
if (signal?.aborted)
|
|
67
|
+
throw new AbortedError();
|
|
68
|
+
};
|
|
58
69
|
try {
|
|
70
|
+
throwIfAborted();
|
|
59
71
|
// Initial API call
|
|
60
72
|
let response = await provider.sendMessage({
|
|
61
73
|
model: config.model,
|
|
@@ -66,11 +78,13 @@ export async function handleMessage(db, userMessage, onProgress) {
|
|
|
66
78
|
thinking: useThinking
|
|
67
79
|
? { type: "enabled", budget_tokens: config.thinkingBudget }
|
|
68
80
|
: undefined,
|
|
81
|
+
signal,
|
|
69
82
|
});
|
|
70
83
|
// Agentic tool loop
|
|
71
84
|
const startTime = Date.now();
|
|
72
85
|
let toolCount = 0;
|
|
73
86
|
while (response.stopReason === "tool_use" && toolCount < MAX_TOOL_STEPS) {
|
|
87
|
+
throwIfAborted();
|
|
74
88
|
messages.push({ role: "assistant", content: response.content });
|
|
75
89
|
const toolResults = [];
|
|
76
90
|
for (const block of response.content) {
|
|
@@ -97,6 +111,7 @@ export async function handleMessage(db, userMessage, onProgress) {
|
|
|
97
111
|
toolCount,
|
|
98
112
|
elapsedMs: Date.now() - startTime,
|
|
99
113
|
});
|
|
114
|
+
throwIfAborted();
|
|
100
115
|
response = await provider.sendMessage({
|
|
101
116
|
model: config.model,
|
|
102
117
|
maxTokens: useThinking ? 16000 : 4096,
|
|
@@ -106,6 +121,7 @@ export async function handleMessage(db, userMessage, onProgress) {
|
|
|
106
121
|
thinking: useThinking
|
|
107
122
|
? { type: "enabled", budget_tokens: config.thinkingBudget }
|
|
108
123
|
: undefined,
|
|
124
|
+
signal,
|
|
109
125
|
});
|
|
110
126
|
}
|
|
111
127
|
// Extract text response, restore PII for display
|
|
@@ -116,6 +132,9 @@ export async function handleMessage(db, userMessage, onProgress) {
|
|
|
116
132
|
return responseText || "I looked into that but couldn't formulate a response. Could you try rephrasing?";
|
|
117
133
|
}
|
|
118
134
|
catch (error) {
|
|
135
|
+
if (error instanceof AbortedError || error?.name === "AbortError" || signal?.aborted) {
|
|
136
|
+
throw new AbortedError();
|
|
137
|
+
}
|
|
119
138
|
if (error.status === 403) {
|
|
120
139
|
if (useManaged()) {
|
|
121
140
|
return "Your API key was rejected. This usually means your subscription is inactive. Run `ray billing` to check your payment status, or `ray setup` to reconfigure.";
|
package/dist/ai/provider.d.ts
CHANGED
|
@@ -17,7 +17,9 @@ export function createAnthropicProvider(opts) {
|
|
|
17
17
|
if (params.thinking) {
|
|
18
18
|
apiParams.thinking = params.thinking;
|
|
19
19
|
}
|
|
20
|
-
const response = await client.messages.create(apiParams
|
|
20
|
+
const response = await client.messages.create(apiParams, {
|
|
21
|
+
signal: params.signal,
|
|
22
|
+
});
|
|
21
23
|
// Filter thinking blocks and normalize content
|
|
22
24
|
const content = [];
|
|
23
25
|
for (const block of response.content) {
|
|
@@ -19,7 +19,7 @@ export function createOpenAICompatibleProvider(opts) {
|
|
|
19
19
|
max_tokens: params.maxTokens,
|
|
20
20
|
messages,
|
|
21
21
|
tools: tools.length > 0 ? tools : undefined,
|
|
22
|
-
});
|
|
22
|
+
}, { signal: params.signal });
|
|
23
23
|
}
|
|
24
24
|
catch (e) {
|
|
25
25
|
if (e.status === 400 && e.message?.includes("max_tokens")) {
|
|
@@ -28,7 +28,7 @@ export function createOpenAICompatibleProvider(opts) {
|
|
|
28
28
|
max_completion_tokens: params.maxTokens,
|
|
29
29
|
messages,
|
|
30
30
|
tools: tools.length > 0 ? tools : undefined,
|
|
31
|
-
});
|
|
31
|
+
}, { signal: params.signal });
|
|
32
32
|
}
|
|
33
33
|
else {
|
|
34
34
|
throw e;
|
package/dist/cli/chat.d.ts
CHANGED
package/dist/cli/chat.js
CHANGED
|
@@ -1,307 +1,16 @@
|
|
|
1
1
|
import chalk from "chalk";
|
|
2
2
|
import { config } from "../config.js";
|
|
3
|
-
import { banner
|
|
4
|
-
/**
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
let cursor = 0; // cursor position within buf
|
|
9
|
-
const out = process.stdout;
|
|
10
|
-
const promptLen = stripAnsi(prompt).length;
|
|
11
|
-
const cols = () => process.stdout.columns || 80;
|
|
12
|
-
// Rows occupied by prompt+text of the given length (accounting for wrap)
|
|
13
|
-
const calcRows = (len) => {
|
|
14
|
-
const total = promptLen + len;
|
|
15
|
-
return Math.max(1, Math.ceil(Math.max(1, total) / cols()));
|
|
16
|
-
};
|
|
17
|
-
// (row, col) of the position at offset `len` from the start of the prompt
|
|
18
|
-
const calcPos = (len) => {
|
|
19
|
-
const w = cols();
|
|
20
|
-
const abs = promptLen + len;
|
|
21
|
-
return { row: Math.floor(abs / w), col: abs % w };
|
|
22
|
-
};
|
|
23
|
-
let renderedRows = 1; // rows the prompt+buf occupy on screen
|
|
24
|
-
let curRow = 0; // current cursor row offset within the prompt area
|
|
25
|
-
// Full re-render: assumes cursor is at (curRow, *) within the prompt area. Moves
|
|
26
|
-
// to the top of the prompt, clears to end of screen, rewrites prompt+buf and
|
|
27
|
-
// belowLines, then positions cursor at `cursor` within buf.
|
|
28
|
-
const render = () => {
|
|
29
|
-
if (curRow > 0)
|
|
30
|
-
out.write(`\x1b[${curRow}A`);
|
|
31
|
-
out.write("\r");
|
|
32
|
-
// Clear from here to end of screen — drops all old wrapped rows AND belowLines
|
|
33
|
-
out.write("\x1b[J");
|
|
34
|
-
out.write(prompt + buf);
|
|
35
|
-
const endPos = calcPos(buf.length);
|
|
36
|
-
let fromRow = endPos.row;
|
|
37
|
-
let fromCol = endPos.col;
|
|
38
|
-
if (belowLines.length > 0) {
|
|
39
|
-
out.write("\n" + belowLines.join("\n"));
|
|
40
|
-
out.write(`\x1b[${belowLines.length}A`);
|
|
41
|
-
out.write("\r");
|
|
42
|
-
fromCol = 0;
|
|
43
|
-
}
|
|
44
|
-
const tgt = calcPos(cursor);
|
|
45
|
-
if (fromRow > tgt.row)
|
|
46
|
-
out.write(`\x1b[${fromRow - tgt.row}A`);
|
|
47
|
-
else if (tgt.row > fromRow)
|
|
48
|
-
out.write(`\x1b[${tgt.row - fromRow}B`);
|
|
49
|
-
if (tgt.col !== fromCol) {
|
|
50
|
-
out.write("\r");
|
|
51
|
-
if (tgt.col > 0)
|
|
52
|
-
out.write(`\x1b[${tgt.col}C`);
|
|
53
|
-
}
|
|
54
|
-
renderedRows = calcRows(buf.length);
|
|
55
|
-
curRow = tgt.row;
|
|
56
|
-
};
|
|
57
|
-
// Find the start of the previous word boundary
|
|
58
|
-
const wordLeft = () => {
|
|
59
|
-
let p = cursor;
|
|
60
|
-
while (p > 0 && buf[p - 1] === " ")
|
|
61
|
-
p--; // skip trailing spaces
|
|
62
|
-
while (p > 0 && buf[p - 1] !== " ")
|
|
63
|
-
p--; // skip word chars
|
|
64
|
-
return p;
|
|
65
|
-
};
|
|
66
|
-
// Find the end of the next word boundary
|
|
67
|
-
const wordRight = () => {
|
|
68
|
-
let p = cursor;
|
|
69
|
-
while (p < buf.length && buf[p] !== " ")
|
|
70
|
-
p++; // skip word chars
|
|
71
|
-
while (p < buf.length && buf[p] === " ")
|
|
72
|
-
p++; // skip trailing spaces
|
|
73
|
-
return p;
|
|
74
|
-
};
|
|
75
|
-
// Initial render
|
|
76
|
-
render();
|
|
77
|
-
process.stdin.setRawMode(true);
|
|
78
|
-
process.stdin.resume();
|
|
79
|
-
process.stdin.setEncoding("utf8");
|
|
80
|
-
const cleanup = () => {
|
|
81
|
-
process.stdin.setRawMode(false);
|
|
82
|
-
process.stdin.removeListener("data", onData);
|
|
83
|
-
process.stdin.pause();
|
|
84
|
-
};
|
|
85
|
-
const onData = (chunk) => {
|
|
86
|
-
for (let i = 0; i < chunk.length; i++) {
|
|
87
|
-
const code = chunk.charCodeAt(i);
|
|
88
|
-
// Ctrl+C / Ctrl+D
|
|
89
|
-
if (code === 3 || code === 4) {
|
|
90
|
-
cleanup();
|
|
91
|
-
out.write("\n");
|
|
92
|
-
resolve({ input: "\x03", rows: renderedRows });
|
|
93
|
-
return;
|
|
94
|
-
}
|
|
95
|
-
// Ctrl+A — beginning of line
|
|
96
|
-
if (code === 1) {
|
|
97
|
-
if (cursor > 0) {
|
|
98
|
-
cursor = 0;
|
|
99
|
-
render();
|
|
100
|
-
}
|
|
101
|
-
continue;
|
|
102
|
-
}
|
|
103
|
-
// Ctrl+E — end of line
|
|
104
|
-
if (code === 5) {
|
|
105
|
-
if (cursor < buf.length) {
|
|
106
|
-
cursor = buf.length;
|
|
107
|
-
render();
|
|
108
|
-
}
|
|
109
|
-
continue;
|
|
110
|
-
}
|
|
111
|
-
// Ctrl+K — delete from cursor to end of line
|
|
112
|
-
if (code === 11) {
|
|
113
|
-
if (cursor < buf.length) {
|
|
114
|
-
buf = buf.slice(0, cursor);
|
|
115
|
-
render();
|
|
116
|
-
}
|
|
117
|
-
continue;
|
|
118
|
-
}
|
|
119
|
-
// Ctrl+U — delete from cursor to beginning of line
|
|
120
|
-
if (code === 21) {
|
|
121
|
-
if (cursor > 0) {
|
|
122
|
-
buf = buf.slice(cursor);
|
|
123
|
-
cursor = 0;
|
|
124
|
-
render();
|
|
125
|
-
}
|
|
126
|
-
continue;
|
|
127
|
-
}
|
|
128
|
-
// Ctrl+W — delete word backward
|
|
129
|
-
if (code === 23) {
|
|
130
|
-
if (cursor > 0) {
|
|
131
|
-
const target = wordLeft();
|
|
132
|
-
buf = buf.slice(0, target) + buf.slice(cursor);
|
|
133
|
-
cursor = target;
|
|
134
|
-
render();
|
|
135
|
-
}
|
|
136
|
-
continue;
|
|
137
|
-
}
|
|
138
|
-
// Enter
|
|
139
|
-
if (code === 13) {
|
|
140
|
-
cleanup();
|
|
141
|
-
// Move cursor to end of buf
|
|
142
|
-
const end = calcPos(buf.length);
|
|
143
|
-
if (end.row > curRow)
|
|
144
|
-
out.write(`\x1b[${end.row - curRow}B`);
|
|
145
|
-
else if (curRow > end.row)
|
|
146
|
-
out.write(`\x1b[${curRow - end.row}A`);
|
|
147
|
-
out.write("\r");
|
|
148
|
-
if (end.col > 0)
|
|
149
|
-
out.write(`\x1b[${end.col}C`);
|
|
150
|
-
// Move past the below-content lines, then newline
|
|
151
|
-
for (let j = 0; j < belowLines.length; j++)
|
|
152
|
-
out.write("\x1b[1B");
|
|
153
|
-
out.write("\n");
|
|
154
|
-
resolve({ input: buf, rows: renderedRows });
|
|
155
|
-
return;
|
|
156
|
-
}
|
|
157
|
-
// Backspace
|
|
158
|
-
if (code === 127 || code === 8) {
|
|
159
|
-
if (cursor > 0) {
|
|
160
|
-
buf = buf.slice(0, cursor - 1) + buf.slice(cursor);
|
|
161
|
-
cursor--;
|
|
162
|
-
render();
|
|
163
|
-
}
|
|
164
|
-
continue;
|
|
165
|
-
}
|
|
166
|
-
// Escape sequences (arrow keys, Option+key, etc.)
|
|
167
|
-
if (code === 27) {
|
|
168
|
-
// Option+Backspace — ESC followed by DEL (0x7f)
|
|
169
|
-
if (i + 1 < chunk.length && chunk.charCodeAt(i + 1) === 127) {
|
|
170
|
-
i++; // consume the DEL
|
|
171
|
-
if (cursor > 0) {
|
|
172
|
-
const target = wordLeft();
|
|
173
|
-
buf = buf.slice(0, target) + buf.slice(cursor);
|
|
174
|
-
cursor = target;
|
|
175
|
-
render();
|
|
176
|
-
}
|
|
177
|
-
continue;
|
|
178
|
-
}
|
|
179
|
-
// Option+b / Option+f — ESC followed by 'b' or 'f'
|
|
180
|
-
if (i + 1 < chunk.length && chunk[i + 1] === "b") {
|
|
181
|
-
i++;
|
|
182
|
-
const target = wordLeft();
|
|
183
|
-
if (target < cursor) {
|
|
184
|
-
cursor = target;
|
|
185
|
-
render();
|
|
186
|
-
}
|
|
187
|
-
continue;
|
|
188
|
-
}
|
|
189
|
-
if (i + 1 < chunk.length && chunk[i + 1] === "f") {
|
|
190
|
-
i++;
|
|
191
|
-
const target = wordRight();
|
|
192
|
-
if (target > cursor) {
|
|
193
|
-
cursor = target;
|
|
194
|
-
render();
|
|
195
|
-
}
|
|
196
|
-
continue;
|
|
197
|
-
}
|
|
198
|
-
if (i + 1 < chunk.length && chunk[i + 1] === "[") {
|
|
199
|
-
i += 2; // skip past ESC [
|
|
200
|
-
// Collect any intermediate bytes (modifiers like "1;3")
|
|
201
|
-
let seq = "";
|
|
202
|
-
while (i < chunk.length && chunk.charCodeAt(i) < 64) {
|
|
203
|
-
seq += chunk[i];
|
|
204
|
-
i++;
|
|
205
|
-
}
|
|
206
|
-
if (i < chunk.length) {
|
|
207
|
-
const final = chunk[i];
|
|
208
|
-
// Modifier keys: ;3 = Option, ;5 = Ctrl, ;9 = Cmd (Kitty protocol)
|
|
209
|
-
const isWordMod = seq === "1;3" || seq === "1;5" || seq === "1;9";
|
|
210
|
-
if (final === "D") {
|
|
211
|
-
if (isWordMod) {
|
|
212
|
-
const target = wordLeft();
|
|
213
|
-
if (target < cursor) {
|
|
214
|
-
cursor = target;
|
|
215
|
-
render();
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
else if (cursor > 0) {
|
|
219
|
-
cursor--;
|
|
220
|
-
render();
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
else if (final === "C") {
|
|
224
|
-
if (isWordMod) {
|
|
225
|
-
const target = wordRight();
|
|
226
|
-
if (target > cursor) {
|
|
227
|
-
cursor = target;
|
|
228
|
-
render();
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
else if (cursor < buf.length) {
|
|
232
|
-
cursor++;
|
|
233
|
-
render();
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
else if (final === "H") {
|
|
237
|
-
if (cursor > 0) {
|
|
238
|
-
cursor = 0;
|
|
239
|
-
render();
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
else if (final === "F") {
|
|
243
|
-
if (cursor < buf.length) {
|
|
244
|
-
cursor = buf.length;
|
|
245
|
-
render();
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
else if (final === "u") {
|
|
249
|
-
// Kitty keyboard protocol: ESC [ codepoint ; modifier u
|
|
250
|
-
const parts = seq.split(";");
|
|
251
|
-
const codepoint = parseInt(parts[0], 10);
|
|
252
|
-
const mod = parts.length > 1 ? parseInt(parts[1], 10) : 1;
|
|
253
|
-
const hasCmd = (mod - 1) & 8; // super/cmd bit
|
|
254
|
-
const hasCtrl = (mod - 1) & 4; // ctrl bit
|
|
255
|
-
if (codepoint === 127 && (hasCmd || hasCtrl)) {
|
|
256
|
-
// Cmd+Backspace / Ctrl+Backspace — delete to line start
|
|
257
|
-
if (cursor > 0) {
|
|
258
|
-
buf = buf.slice(cursor);
|
|
259
|
-
cursor = 0;
|
|
260
|
-
render();
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
// Ignore other sequences (up/down, etc.)
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
continue;
|
|
268
|
-
}
|
|
269
|
-
// Printable characters
|
|
270
|
-
if (code >= 32) {
|
|
271
|
-
buf = buf.slice(0, cursor) + chunk[i] + buf.slice(cursor);
|
|
272
|
-
cursor++;
|
|
273
|
-
render();
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
};
|
|
277
|
-
process.stdin.on("data", onData);
|
|
278
|
-
});
|
|
279
|
-
}
|
|
280
|
-
/** Strip ANSI escape codes to get visible length */
|
|
281
|
-
function stripAnsi(str) {
|
|
282
|
-
return str.replace(/\x1b\[[0-9;]*m/g, "");
|
|
283
|
-
}
|
|
284
|
-
const THINKING_PHRASES = [
|
|
285
|
-
"Thinking...",
|
|
286
|
-
"Crunching numbers...",
|
|
287
|
-
"Reviewing your accounts...",
|
|
288
|
-
"Analyzing...",
|
|
289
|
-
"Looking into that...",
|
|
290
|
-
"Pulling up your data...",
|
|
291
|
-
"Checking the numbers...",
|
|
292
|
-
"On it...",
|
|
293
|
-
];
|
|
294
|
-
function getThinkingText() {
|
|
295
|
-
return THINKING_PHRASES[Math.floor(Math.random() * THINKING_PHRASES.length)];
|
|
296
|
-
}
|
|
3
|
+
import { banner } from "./format.js";
|
|
4
|
+
/**
|
|
5
|
+
* Pre-mount orchestration: banner, briefing, account check + optional runLink,
|
|
6
|
+
* then hand off to the Ink-rendered ChatApp.
|
|
7
|
+
*/
|
|
297
8
|
export async function startChat() {
|
|
298
|
-
const ora = (await import("ora")).default;
|
|
299
9
|
const { getDb } = await import("../db/connection.js");
|
|
300
|
-
const { handleMessage, TOOL_LABELS } = await import("../ai/agent.js");
|
|
301
10
|
const { isContextEmpty } = await import("../ai/context.js");
|
|
302
11
|
const { cliBriefing } = await import("../ai/insights.js");
|
|
303
12
|
const db = getDb();
|
|
304
|
-
//
|
|
13
|
+
// Banner + briefing (plain stdout — becomes scrollback above the Ink region)
|
|
305
14
|
console.log("");
|
|
306
15
|
console.log(banner());
|
|
307
16
|
console.log("");
|
|
@@ -327,143 +36,18 @@ export async function startChat() {
|
|
|
327
36
|
console.log(chalk.yellow("No accounts linked yet. Let's connect one first.\n"));
|
|
328
37
|
const { runLink } = await import("./commands.js");
|
|
329
38
|
await runLink();
|
|
330
|
-
// Re-check after linking
|
|
331
39
|
const recheck = db.prepare("SELECT COUNT(*) as count FROM accounts").get();
|
|
332
40
|
if (recheck.count === 0) {
|
|
333
41
|
console.log(chalk.red("\nNo accounts linked. Run 'ray link' when you're ready.\n"));
|
|
334
42
|
return;
|
|
335
43
|
}
|
|
336
44
|
}
|
|
337
|
-
//
|
|
45
|
+
// Fire onboarding if context is empty
|
|
46
|
+
let onboardingPrompt;
|
|
338
47
|
if (isContextEmpty()) {
|
|
339
48
|
console.log(chalk.yellowBright("Welcome! Let me review your accounts and help set up your financial profile.\n"));
|
|
340
|
-
|
|
341
|
-
try {
|
|
342
|
-
const response = await handleMessage(db, "I just connected my financial accounts. Help me set up my financial profile.");
|
|
343
|
-
spinner.stop();
|
|
344
|
-
console.log(`\n${response}\n`);
|
|
345
|
-
}
|
|
346
|
-
catch (err) {
|
|
347
|
-
spinner.stop();
|
|
348
|
-
console.error(formatError(err, "Onboarding error"));
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
// Background re-sync for recently linked accounts (Plaid backfill can take hours)
|
|
352
|
-
let bgSyncTimer = null;
|
|
353
|
-
const oldestAccount = db.prepare(`SELECT MIN(created_at) as ts FROM institutions`).get();
|
|
354
|
-
if (oldestAccount?.ts) {
|
|
355
|
-
const ageMs = Date.now() - new Date(oldestAccount.ts + "Z").getTime();
|
|
356
|
-
if (ageMs < 6 * 60 * 60 * 1000) { // linked within last 6 hours
|
|
357
|
-
const { runDailySync } = await import("../daily-sync.js");
|
|
358
|
-
bgSyncTimer = setInterval(async () => {
|
|
359
|
-
// Silence all output during background sync
|
|
360
|
-
const origWrite = process.stdout.write;
|
|
361
|
-
const origErr = process.stderr.write;
|
|
362
|
-
process.stdout.write = () => true;
|
|
363
|
-
process.stderr.write = () => true;
|
|
364
|
-
try {
|
|
365
|
-
await runDailySync(db);
|
|
366
|
-
}
|
|
367
|
-
catch { }
|
|
368
|
-
process.stdout.write = origWrite;
|
|
369
|
-
process.stderr.write = origErr;
|
|
370
|
-
}, 15 * 60 * 1000); // every 15 minutes
|
|
371
|
-
bgSyncTimer.unref(); // don't prevent process exit
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
const shutdown = () => {
|
|
375
|
-
if (bgSyncTimer)
|
|
376
|
-
clearInterval(bgSyncTimer);
|
|
377
|
-
console.log(chalk.dim("\nGoodbye!"));
|
|
378
|
-
process.exit(0);
|
|
379
|
-
};
|
|
380
|
-
const hints = [
|
|
381
|
-
"try: how am i doing this month?",
|
|
382
|
-
"try: where's my money going?",
|
|
383
|
-
"try: what bills are coming up?",
|
|
384
|
-
"try: help me save more",
|
|
385
|
-
"try: am i on track for my goals?",
|
|
386
|
-
"try: any unusual spending lately?",
|
|
387
|
-
"try: what should i focus on?",
|
|
388
|
-
"try: compare this month to last month",
|
|
389
|
-
"try: set a budget for dining out",
|
|
390
|
-
"try: how much did i spend on groceries?",
|
|
391
|
-
];
|
|
392
|
-
let hintIdx = Math.floor(Math.random() * hints.length);
|
|
393
|
-
const getFooterText = () => {
|
|
394
|
-
const lastSync = db.prepare(`SELECT MAX(updated_at) as ts FROM accounts`).get();
|
|
395
|
-
let syncStr = "";
|
|
396
|
-
if (lastSync.ts) {
|
|
397
|
-
const diffMs = Date.now() - new Date(lastSync.ts + "Z").getTime();
|
|
398
|
-
const mins = Math.floor(diffMs / 60000);
|
|
399
|
-
if (mins < 1)
|
|
400
|
-
syncStr = "synced just now";
|
|
401
|
-
else if (mins < 60)
|
|
402
|
-
syncStr = `synced ${mins}m ago`;
|
|
403
|
-
else if (mins < 1440)
|
|
404
|
-
syncStr = `synced ${Math.floor(mins / 60)}h ago`;
|
|
405
|
-
else
|
|
406
|
-
syncStr = `synced ${Math.floor(mins / 1440)}d ago`;
|
|
407
|
-
}
|
|
408
|
-
const parts = ["ray"];
|
|
409
|
-
if (syncStr)
|
|
410
|
-
parts.push(syncStr);
|
|
411
|
-
parts.push(hints[hintIdx]);
|
|
412
|
-
parts.push("ctrl+c to exit");
|
|
413
|
-
hintIdx = (hintIdx + 1) % hints.length;
|
|
414
|
-
return parts.join(" · ");
|
|
415
|
-
};
|
|
416
|
-
while (true) {
|
|
417
|
-
const cols = process.stdout.columns || 80;
|
|
418
|
-
const rule = chalk.dim("─".repeat(cols));
|
|
419
|
-
const footerText = chalk.dim(` ${getFooterText()}`);
|
|
420
|
-
// Ensure room below for top rule + prompt + bottom rule + footer (3 lines below start)
|
|
421
|
-
process.stdout.write("\n\n\n");
|
|
422
|
-
process.stdout.write("\x1b[3A\r");
|
|
423
|
-
// Print top rule, then prompt with bottom rule + footer rendered below
|
|
424
|
-
console.log(rule);
|
|
425
|
-
const { input, rows: promptRows } = await rawReadLine(chalk.dim("❯ "), [rule, footerText]);
|
|
426
|
-
const trimmed = input.trim();
|
|
427
|
-
if (!trimmed) {
|
|
428
|
-
// Clear the prompt frame (prompt + bottom rule + footer); leave top rule
|
|
429
|
-
process.stdout.write(`\x1b[${promptRows + 2}A\r`);
|
|
430
|
-
for (let i = 0; i < promptRows + 3; i++)
|
|
431
|
-
process.stdout.write("\x1b[2K\x1b[1B");
|
|
432
|
-
process.stdout.write(`\x1b[${promptRows + 3}A\r`);
|
|
433
|
-
continue;
|
|
434
|
-
}
|
|
435
|
-
// Replace prompt frame (top rule + prompt + bottom rule + footer) with gray-bg user message
|
|
436
|
-
const frameRows = promptRows + 3;
|
|
437
|
-
process.stdout.write(`\x1b[${frameRows}A\r`);
|
|
438
|
-
for (let i = 0; i < frameRows; i++)
|
|
439
|
-
process.stdout.write("\x1b[2K\x1b[1B");
|
|
440
|
-
process.stdout.write(`\x1b[${frameRows}A\r`);
|
|
441
|
-
// Print user message with gray background, padded to full width
|
|
442
|
-
const msgText = `❯ ${trimmed}`;
|
|
443
|
-
const pad = Math.max(0, cols - msgText.length);
|
|
444
|
-
console.log(chalk.bgGray.white(msgText + " ".repeat(pad)));
|
|
445
|
-
if (trimmed === "\x03" || trimmed === "/quit" || trimmed === "/exit" || trimmed === "/q") {
|
|
446
|
-
shutdown();
|
|
447
|
-
break;
|
|
448
|
-
}
|
|
449
|
-
const spinner = ora({ text: getThinkingText(), color: "cyan", discardStdin: false }).start();
|
|
450
|
-
const onProgress = ({ phase, toolName, toolCount, elapsedMs }) => {
|
|
451
|
-
if (phase === "tool" && toolName) {
|
|
452
|
-
const label = TOOL_LABELS[toolName] || toolName;
|
|
453
|
-
spinner.text = `${label}... ${chalk.dim(`(${toolCount} ${toolCount === 1 ? "tool" : "tools"}, ${formatDuration(elapsedMs)})`)}`;
|
|
454
|
-
}
|
|
455
|
-
else if (phase === "responding" && toolCount > 0) {
|
|
456
|
-
spinner.text = `Composing response... ${chalk.dim(`(${toolCount} tools, ${formatDuration(elapsedMs)})`)}`;
|
|
457
|
-
}
|
|
458
|
-
};
|
|
459
|
-
try {
|
|
460
|
-
const response = await handleMessage(db, trimmed, onProgress);
|
|
461
|
-
spinner.stop();
|
|
462
|
-
console.log(`\n${formatResponse(response)}\n`);
|
|
463
|
-
}
|
|
464
|
-
catch (err) {
|
|
465
|
-
spinner.stop();
|
|
466
|
-
console.error(formatError(err));
|
|
467
|
-
}
|
|
49
|
+
onboardingPrompt = "I just connected my financial accounts. Help me set up my financial profile.";
|
|
468
50
|
}
|
|
51
|
+
const { runChatApp } = await import("./ink/mount.js");
|
|
52
|
+
await runChatApp({ db, onboardingPrompt });
|
|
469
53
|
}
|
package/dist/cli/commands.js
CHANGED
|
@@ -481,7 +481,7 @@ export function showRecap(period = "last_month") {
|
|
|
481
481
|
// Income this period
|
|
482
482
|
const income = db.prepare(`SELECT COALESCE(SUM(ABS(amount)), 0) as total FROM transactions
|
|
483
483
|
WHERE amount < 0 AND date BETWEEN ? AND ? AND pending = 0
|
|
484
|
-
AND category NOT IN ('TRANSFER_IN')`).get(start, end);
|
|
484
|
+
AND category NOT IN ('TRANSFER_IN', 'LOAN_PAYMENTS', 'LOAN_PAYMENTS_CAR_PAYMENT', 'LOAN_PAYMENTS_PERSONAL_LOAN_PAYMENT')`).get(start, end);
|
|
485
485
|
const totalSpent = spending.total || 0;
|
|
486
486
|
const txnCount = spending.count || 0;
|
|
487
487
|
if (txnCount === 0) {
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type Database from "libsql";
|
|
2
|
+
interface Props {
|
|
3
|
+
db: Database.Database;
|
|
4
|
+
/** Auto-kick-off message to send silently on mount (onboarding). */
|
|
5
|
+
onboardingPrompt?: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function ChatApp({ db, onboardingPrompt }: Props): import("react/jsx-runtime").JSX.Element;
|
|
8
|
+
export {};
|