kalshi-trading-bot-cli 2.1.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/LICENSE +21 -0
- package/README.md +360 -0
- package/assets/kalshi-flow-light.png +0 -0
- package/assets/screenshot.png +0 -0
- package/env.example +43 -0
- package/kalshi-flow-light.png +0 -0
- package/package.json +66 -0
- package/src/agent/agent.ts +249 -0
- package/src/agent/channels.ts +53 -0
- package/src/agent/index.ts +29 -0
- package/src/agent/prompts.ts +171 -0
- package/src/agent/run-context.ts +23 -0
- package/src/agent/scratchpad.ts +465 -0
- package/src/agent/token-counter.ts +33 -0
- package/src/agent/tool-executor.ts +166 -0
- package/src/agent/types.ts +221 -0
- package/src/audit/index.ts +25 -0
- package/src/audit/reader.ts +43 -0
- package/src/audit/trail.ts +29 -0
- package/src/audit/types.ts +133 -0
- package/src/backtest/discovery.ts +170 -0
- package/src/backtest/fetcher.ts +247 -0
- package/src/backtest/metrics.ts +165 -0
- package/src/backtest/renderer.ts +196 -0
- package/src/backtest/types.ts +45 -0
- package/src/cli.ts +943 -0
- package/src/commands/alerts.ts +48 -0
- package/src/commands/analyze.ts +662 -0
- package/src/commands/backtest.ts +276 -0
- package/src/commands/clear-cache.ts +24 -0
- package/src/commands/config.ts +107 -0
- package/src/commands/dispatch.ts +473 -0
- package/src/commands/edge.ts +62 -0
- package/src/commands/formatters.ts +339 -0
- package/src/commands/help.ts +263 -0
- package/src/commands/helpers.ts +48 -0
- package/src/commands/index.ts +287 -0
- package/src/commands/json.ts +43 -0
- package/src/commands/parse-args.ts +229 -0
- package/src/commands/portfolio.ts +236 -0
- package/src/commands/review.ts +176 -0
- package/src/commands/scan-formatters.ts +98 -0
- package/src/commands/scan.ts +38 -0
- package/src/commands/search-edge.ts +139 -0
- package/src/commands/status.ts +70 -0
- package/src/commands/themes.ts +117 -0
- package/src/commands/watch.ts +295 -0
- package/src/components/answer-box.ts +57 -0
- package/src/components/approval-prompt.ts +34 -0
- package/src/components/browse-list.ts +134 -0
- package/src/components/chat-log.ts +291 -0
- package/src/components/custom-editor.ts +18 -0
- package/src/components/debug-panel.ts +52 -0
- package/src/components/index.ts +17 -0
- package/src/components/intro.ts +92 -0
- package/src/components/select-list.ts +155 -0
- package/src/components/tool-event.ts +127 -0
- package/src/components/user-query.ts +18 -0
- package/src/components/working-indicator.ts +87 -0
- package/src/controllers/agent-runner.ts +283 -0
- package/src/controllers/browse.ts +1013 -0
- package/src/controllers/index.ts +7 -0
- package/src/controllers/input-history.ts +76 -0
- package/src/controllers/model-selection.ts +244 -0
- package/src/db/alerts.ts +77 -0
- package/src/db/edge.ts +105 -0
- package/src/db/event-index.ts +323 -0
- package/src/db/events.ts +41 -0
- package/src/db/index.ts +60 -0
- package/src/db/octagon-cache.ts +118 -0
- package/src/db/positions.ts +71 -0
- package/src/db/risk.ts +51 -0
- package/src/db/schema.ts +227 -0
- package/src/db/themes.ts +34 -0
- package/src/db/trades.ts +50 -0
- package/src/eval/brier.ts +90 -0
- package/src/eval/index.ts +4 -0
- package/src/eval/performance.ts +87 -0
- package/src/gateway/access-control.ts +253 -0
- package/src/gateway/agent-runner.ts +75 -0
- package/src/gateway/alerts/formatter.ts +90 -0
- package/src/gateway/alerts/index.ts +4 -0
- package/src/gateway/alerts/router.ts +32 -0
- package/src/gateway/alerts/terminal.ts +16 -0
- package/src/gateway/alerts/types.ts +13 -0
- package/src/gateway/channels/index.ts +9 -0
- package/src/gateway/channels/manager.ts +153 -0
- package/src/gateway/channels/types.ts +48 -0
- package/src/gateway/channels/whatsapp/README.md +234 -0
- package/src/gateway/channels/whatsapp/auth-store.ts +140 -0
- package/src/gateway/channels/whatsapp/dedupe.ts +60 -0
- package/src/gateway/channels/whatsapp/error.ts +122 -0
- package/src/gateway/channels/whatsapp/inbound.ts +326 -0
- package/src/gateway/channels/whatsapp/index.ts +5 -0
- package/src/gateway/channels/whatsapp/lid.ts +56 -0
- package/src/gateway/channels/whatsapp/logger.ts +25 -0
- package/src/gateway/channels/whatsapp/login.ts +94 -0
- package/src/gateway/channels/whatsapp/outbound.ts +119 -0
- package/src/gateway/channels/whatsapp/plugin.ts +54 -0
- package/src/gateway/channels/whatsapp/reconnect.ts +40 -0
- package/src/gateway/channels/whatsapp/runtime.ts +122 -0
- package/src/gateway/channels/whatsapp/session.ts +89 -0
- package/src/gateway/channels/whatsapp/types.ts +32 -0
- package/src/gateway/commands/handler.ts +64 -0
- package/src/gateway/commands/index.ts +7 -0
- package/src/gateway/commands/parser.ts +29 -0
- package/src/gateway/commands/wa-formatters.ts +92 -0
- package/src/gateway/config.ts +244 -0
- package/src/gateway/extension-points.ts +17 -0
- package/src/gateway/gateway.ts +301 -0
- package/src/gateway/group/history-buffer.ts +75 -0
- package/src/gateway/group/index.ts +8 -0
- package/src/gateway/group/member-tracker.ts +60 -0
- package/src/gateway/group/mention-detection.ts +42 -0
- package/src/gateway/heartbeat/index.ts +8 -0
- package/src/gateway/heartbeat/prompt.ts +73 -0
- package/src/gateway/heartbeat/runner.ts +200 -0
- package/src/gateway/heartbeat/suppression.ts +74 -0
- package/src/gateway/index.ts +138 -0
- package/src/gateway/routing/resolve-route.ts +119 -0
- package/src/gateway/sessions/store.ts +65 -0
- package/src/gateway/types.ts +11 -0
- package/src/gateway/utils.ts +82 -0
- package/src/index.tsx +30 -0
- package/src/model/llm.ts +247 -0
- package/src/providers.ts +94 -0
- package/src/risk/circuit-breaker.ts +113 -0
- package/src/risk/correlation.ts +40 -0
- package/src/risk/gate.ts +125 -0
- package/src/risk/index.ts +10 -0
- package/src/risk/kelly.ts +230 -0
- package/src/scan/alerter.ts +64 -0
- package/src/scan/edge-computer.ts +164 -0
- package/src/scan/invoker.ts +199 -0
- package/src/scan/loop.ts +184 -0
- package/src/scan/octagon-client.ts +627 -0
- package/src/scan/octagon-events-api.ts +105 -0
- package/src/scan/octagon-prefetch.ts +172 -0
- package/src/scan/theme-resolver.ts +179 -0
- package/src/scan/types.ts +62 -0
- package/src/scan/watchdog.ts +126 -0
- package/src/setup/wizard.ts +659 -0
- package/src/theme.ts +67 -0
- package/src/tools/fetch/cache.ts +95 -0
- package/src/tools/fetch/external-content.ts +200 -0
- package/src/tools/fetch/index.ts +1 -0
- package/src/tools/fetch/web-fetch-utils.ts +122 -0
- package/src/tools/fetch/web-fetch.ts +419 -0
- package/src/tools/index.ts +10 -0
- package/src/tools/kalshi/api.ts +251 -0
- package/src/tools/kalshi/dlq.ts +35 -0
- package/src/tools/kalshi/events.ts +84 -0
- package/src/tools/kalshi/exchange.ts +24 -0
- package/src/tools/kalshi/historical.ts +89 -0
- package/src/tools/kalshi/index.ts +11 -0
- package/src/tools/kalshi/kalshi-search.ts +437 -0
- package/src/tools/kalshi/kalshi-trade.ts +102 -0
- package/src/tools/kalshi/markets.ts +76 -0
- package/src/tools/kalshi/portfolio.ts +100 -0
- package/src/tools/kalshi/search-index.ts +198 -0
- package/src/tools/kalshi/series.ts +16 -0
- package/src/tools/kalshi/trading.ts +115 -0
- package/src/tools/kalshi/types.ts +199 -0
- package/src/tools/registry.ts +160 -0
- package/src/tools/search/index.ts +25 -0
- package/src/tools/search/tavily.ts +35 -0
- package/src/tools/types.ts +53 -0
- package/src/tools/v2/edge-query.ts +135 -0
- package/src/tools/v2/octagon-report.ts +112 -0
- package/src/tools/v2/portfolio-query.ts +79 -0
- package/src/tools/v2/portfolio-review.ts +59 -0
- package/src/tools/v2/risk-status.ts +94 -0
- package/src/tools/v2/scan.ts +78 -0
- package/src/types/qrcode-terminal.d.ts +7 -0
- package/src/types/whiskeysockets-baileys.d.ts +41 -0
- package/src/types.ts +22 -0
- package/src/utils/ai-message.ts +26 -0
- package/src/utils/bot-config.ts +219 -0
- package/src/utils/cache.ts +195 -0
- package/src/utils/config.ts +113 -0
- package/src/utils/env.ts +111 -0
- package/src/utils/errors.ts +313 -0
- package/src/utils/history-context.ts +32 -0
- package/src/utils/in-memory-chat-history.ts +268 -0
- package/src/utils/index.ts +28 -0
- package/src/utils/input-key-handlers.ts +64 -0
- package/src/utils/logger.ts +67 -0
- package/src/utils/long-term-chat-history.ts +138 -0
- package/src/utils/markdown-table.ts +227 -0
- package/src/utils/model.ts +70 -0
- package/src/utils/ollama.ts +37 -0
- package/src/utils/paths.ts +12 -0
- package/src/utils/progress-channel.ts +84 -0
- package/src/utils/telemetry.ts +103 -0
- package/src/utils/text-navigation.ts +81 -0
- package/src/utils/thinking-verbs.ts +18 -0
- package/src/utils/tokens.ts +36 -0
- package/src/utils/tool-description.ts +61 -0
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { Container, Input, SelectList, Text, type SelectItem, getEditorKeybindings, truncateToWidth } from '@mariozechner/pi-tui';
|
|
2
|
+
import { PROVIDERS, type Model } from '../utils/model.js';
|
|
3
|
+
import type { ApprovalDecision } from '../agent/types.js';
|
|
4
|
+
import { selectListTheme, theme } from '../theme.js';
|
|
5
|
+
|
|
6
|
+
export class VimSelectList extends SelectList {
|
|
7
|
+
handleInput(keyData: string): void {
|
|
8
|
+
if (keyData === 'j') {
|
|
9
|
+
super.handleInput('\u001b[B');
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
if (keyData === 'k') {
|
|
13
|
+
super.handleInput('\u001b[A');
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
// Number keys: jump to item and select it (1-indexed)
|
|
17
|
+
if (keyData >= '1' && keyData <= '9') {
|
|
18
|
+
const idx = parseInt(keyData, 10) - 1;
|
|
19
|
+
const items = (this as any).items as SelectItem[];
|
|
20
|
+
if (idx < items.length) {
|
|
21
|
+
this.setSelectedIndex(idx);
|
|
22
|
+
this.onSelect?.(items[idx]);
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
super.handleInput(keyData);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
class EmptyModelSelector extends Container {
|
|
31
|
+
private readonly onCancel: () => void;
|
|
32
|
+
|
|
33
|
+
constructor(providerId: string, onCancel: () => void) {
|
|
34
|
+
super();
|
|
35
|
+
this.onCancel = onCancel;
|
|
36
|
+
this.addChild(new Text(theme.muted('No models available.'), 0, 0));
|
|
37
|
+
if (providerId === 'ollama') {
|
|
38
|
+
this.addChild(
|
|
39
|
+
new Text(theme.muted('Make sure Ollama is running and you have models downloaded.'), 0, 0),
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
this.addChild(new Text(theme.muted('esc to go back'), 0, 0));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
handleInput(keyData: string): void {
|
|
46
|
+
const kb = getEditorKeybindings();
|
|
47
|
+
if (kb.matches(keyData, 'selectCancel')) {
|
|
48
|
+
this.onCancel();
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function createProviderSelector(
|
|
54
|
+
currentProvider: string | undefined,
|
|
55
|
+
onSelect: (providerId: string | null) => void,
|
|
56
|
+
) {
|
|
57
|
+
const items: SelectItem[] = PROVIDERS.map((provider, index) => ({
|
|
58
|
+
value: provider.providerId,
|
|
59
|
+
label: `${index + 1}. ${provider.displayName}${currentProvider === provider.providerId ? ' ✓' : ''}`,
|
|
60
|
+
}));
|
|
61
|
+
const list = new VimSelectList(items, 8, selectListTheme);
|
|
62
|
+
list.onSelect = (item) => onSelect(item.value);
|
|
63
|
+
list.onCancel = () => onSelect(null);
|
|
64
|
+
return list;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function createModelSelector(
|
|
68
|
+
models: Model[],
|
|
69
|
+
currentModel: string | undefined,
|
|
70
|
+
onSelect: (modelId: string | null) => void,
|
|
71
|
+
providerId?: string,
|
|
72
|
+
) {
|
|
73
|
+
if (models.length === 0) {
|
|
74
|
+
return new EmptyModelSelector(providerId ?? '', () => onSelect(null));
|
|
75
|
+
}
|
|
76
|
+
const items: SelectItem[] = models.map((model, index) => ({
|
|
77
|
+
value: model.id,
|
|
78
|
+
label: `${index + 1}. ${model.displayName}${currentModel === model.id ? ' ✓' : ''}`,
|
|
79
|
+
}));
|
|
80
|
+
const list = new VimSelectList(items, 10, selectListTheme);
|
|
81
|
+
list.onSelect = (item) => onSelect(item.value);
|
|
82
|
+
list.onCancel = () => onSelect(null);
|
|
83
|
+
return list;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function createApprovalSelector(onSelect: (decision: ApprovalDecision) => void) {
|
|
87
|
+
const items: SelectItem[] = [
|
|
88
|
+
{ value: 'allow-once', label: '1. Yes' },
|
|
89
|
+
{ value: 'allow-session', label: '2. Yes, allow all edits this session' },
|
|
90
|
+
{ value: 'deny', label: '3. No' },
|
|
91
|
+
];
|
|
92
|
+
const list = new VimSelectList(items, 5, selectListTheme);
|
|
93
|
+
list.onSelect = (item) => onSelect(item.value as ApprovalDecision);
|
|
94
|
+
list.onCancel = () => onSelect('deny');
|
|
95
|
+
return list;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function createApiKeyConfirmSelector(onConfirm: (wantsToSet: boolean) => void) {
|
|
99
|
+
const items: SelectItem[] = [
|
|
100
|
+
{ value: 'yes', label: '1. Yes' },
|
|
101
|
+
{ value: 'no', label: '2. No' },
|
|
102
|
+
];
|
|
103
|
+
const list = new VimSelectList(items, 4, selectListTheme);
|
|
104
|
+
list.onSelect = (item) => onConfirm(item.value === 'yes');
|
|
105
|
+
list.onCancel = () => onConfirm(false);
|
|
106
|
+
return list;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export class ApiKeyInputComponent {
|
|
110
|
+
private readonly input = new Input();
|
|
111
|
+
private readonly masked: boolean;
|
|
112
|
+
onSubmit?: (apiKey: string | null) => void;
|
|
113
|
+
onCancel?: () => void;
|
|
114
|
+
|
|
115
|
+
constructor(masked = false) {
|
|
116
|
+
this.masked = masked;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
invalidate() {
|
|
120
|
+
this.input.invalidate();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
render(width: number): string[] {
|
|
124
|
+
const maxContent = Math.max(10, width - 4); // account for "> " prefix
|
|
125
|
+
const lines = this.input.render(maxContent);
|
|
126
|
+
const raw = lines[0] ?? '';
|
|
127
|
+
let display: string;
|
|
128
|
+
if (this.masked) {
|
|
129
|
+
const len = this.input.getValue().length;
|
|
130
|
+
display = len === 0 ? '█' : '*'.repeat(Math.min(len, maxContent));
|
|
131
|
+
} else {
|
|
132
|
+
display = raw;
|
|
133
|
+
}
|
|
134
|
+
return [
|
|
135
|
+
truncateToWidth(`${theme.primary('> ')}${display}`, width),
|
|
136
|
+
];
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
handleInput(keyData: string): void {
|
|
140
|
+
const kb = getEditorKeybindings();
|
|
141
|
+
if (kb.matches(keyData, 'submit')) {
|
|
142
|
+
this.onSubmit?.(this.input.getValue().trim() || null);
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
if (kb.matches(keyData, 'selectCancel')) {
|
|
146
|
+
this.onCancel?.();
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
this.input.handleInput(keyData);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
getValue(): string {
|
|
153
|
+
return this.input.getValue();
|
|
154
|
+
}
|
|
155
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { Container, Spacer, Text } from '@mariozechner/pi-tui';
|
|
2
|
+
import type { ApprovalDecision } from '../agent/types.js';
|
|
3
|
+
import { theme } from '../theme.js';
|
|
4
|
+
|
|
5
|
+
function formatToolName(name: string): string {
|
|
6
|
+
return name
|
|
7
|
+
.split('_')
|
|
8
|
+
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
|
9
|
+
.join(' ');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function truncateAtWord(str: string, maxLength: number): string {
|
|
13
|
+
if (str.length <= maxLength) {
|
|
14
|
+
return str;
|
|
15
|
+
}
|
|
16
|
+
const lastSpace = str.lastIndexOf(' ', maxLength);
|
|
17
|
+
if (lastSpace > maxLength * 0.5) {
|
|
18
|
+
return `${str.slice(0, lastSpace)}...`;
|
|
19
|
+
}
|
|
20
|
+
return `${str.slice(0, maxLength)}...`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function formatArgs(tool: string, args: Record<string, unknown>): string {
|
|
24
|
+
if ('query' in args) {
|
|
25
|
+
const query = String(args.query);
|
|
26
|
+
return theme.muted(`"${truncateAtWord(query, 60)}"`);
|
|
27
|
+
}
|
|
28
|
+
if (tool === 'memory_update') {
|
|
29
|
+
const text = String(args.content ?? args.old_text ?? '').replace(/\n/g, ' ');
|
|
30
|
+
if (text) return theme.muted(truncateAtWord(text, 80));
|
|
31
|
+
}
|
|
32
|
+
return theme.muted(
|
|
33
|
+
Object.entries(args)
|
|
34
|
+
.map(([key, value]) => `${key}=${truncateAtWord(String(value).replace(/\n/g, '\\n'), 60)}`)
|
|
35
|
+
.join(', '),
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function formatDuration(ms: number): string {
|
|
40
|
+
if (ms < 1000) {
|
|
41
|
+
return `${ms}ms`;
|
|
42
|
+
}
|
|
43
|
+
return `${(ms / 1000).toFixed(1)}s`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function approvalLabel(decision: ApprovalDecision): string {
|
|
47
|
+
switch (decision) {
|
|
48
|
+
case 'allow-once':
|
|
49
|
+
return 'Approved';
|
|
50
|
+
case 'allow-session':
|
|
51
|
+
return 'Approved (session)';
|
|
52
|
+
case 'deny':
|
|
53
|
+
return 'Denied';
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export class ToolEventComponent extends Container {
|
|
58
|
+
private readonly header: Text;
|
|
59
|
+
private completedDetails: Text[] = [];
|
|
60
|
+
private activeDetail: Text | null = null;
|
|
61
|
+
|
|
62
|
+
constructor(_tui: unknown, tool: string, args: Record<string, unknown>) {
|
|
63
|
+
super();
|
|
64
|
+
this.addChild(new Spacer(1));
|
|
65
|
+
const title = `${formatToolName(tool)}${args ? `${theme.muted('(')}${formatArgs(tool, args)}${theme.muted(')')}` : ''}`;
|
|
66
|
+
this.header = new Text(`⏺ ${title}`, 0, 0);
|
|
67
|
+
this.addChild(this.header);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
setActive(progressMessage?: string) {
|
|
71
|
+
this.clearDetail();
|
|
72
|
+
const message = progressMessage || 'Searching...';
|
|
73
|
+
this.activeDetail = new Text(`${theme.muted('⎿ ')}${message}`, 0, 0);
|
|
74
|
+
this.addChild(this.activeDetail);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
setComplete(summary: string, duration: number) {
|
|
78
|
+
this.clearDetail();
|
|
79
|
+
const detail = new Text(
|
|
80
|
+
`${theme.muted('⎿ ')}${summary}${theme.muted(` in ${formatDuration(duration)}`)}`,
|
|
81
|
+
0,
|
|
82
|
+
0
|
|
83
|
+
);
|
|
84
|
+
this.completedDetails.push(detail);
|
|
85
|
+
this.addChild(detail);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
setError(error: string) {
|
|
89
|
+
this.clearDetail();
|
|
90
|
+
const detail = new Text(`${theme.muted('⎿ ')}${theme.error(`Error: ${truncateAtWord(error, 80)}`)}`, 0, 0);
|
|
91
|
+
this.completedDetails.push(detail);
|
|
92
|
+
this.addChild(detail);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
setLimitWarning(warning?: string) {
|
|
96
|
+
this.clearDetail();
|
|
97
|
+
this.activeDetail = new Text(
|
|
98
|
+
`${theme.muted('⎿ ')}${theme.warning(truncateAtWord(warning || 'Approaching suggested limit', 100))}`,
|
|
99
|
+
0,
|
|
100
|
+
0,
|
|
101
|
+
);
|
|
102
|
+
this.addChild(this.activeDetail);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
setDenied(path: string, tool: string) {
|
|
106
|
+
this.clearDetail();
|
|
107
|
+
const action = tool === 'write_file' ? 'write to' : tool === 'edit_file' ? 'edit of' : tool;
|
|
108
|
+
const detail = new Text(`${theme.muted('⎿ ')}${theme.warning(`User denied ${action} ${path}`)}`, 0, 0);
|
|
109
|
+
this.completedDetails.push(detail);
|
|
110
|
+
this.addChild(detail);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
setApproval(decision: ApprovalDecision) {
|
|
114
|
+
this.clearDetail();
|
|
115
|
+
const color = decision !== 'deny' ? theme.primary : theme.warning;
|
|
116
|
+
const detail = new Text(`${theme.muted('⎿ ')}${color(approvalLabel(decision))}`, 0, 0);
|
|
117
|
+
this.completedDetails.push(detail);
|
|
118
|
+
this.addChild(detail);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
private clearDetail() {
|
|
122
|
+
if (this.activeDetail) {
|
|
123
|
+
this.removeChild(this.activeDetail);
|
|
124
|
+
this.activeDetail = null;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Container, Spacer, Text } from '@mariozechner/pi-tui';
|
|
2
|
+
import { theme } from '../theme.js';
|
|
3
|
+
|
|
4
|
+
export class UserQueryComponent extends Container {
|
|
5
|
+
private readonly body: Text;
|
|
6
|
+
|
|
7
|
+
constructor(query: string) {
|
|
8
|
+
super();
|
|
9
|
+
this.addChild(new Spacer(1));
|
|
10
|
+
this.body = new Text('', 0, 0);
|
|
11
|
+
this.addChild(this.body);
|
|
12
|
+
this.setQuery(query);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
setQuery(query: string) {
|
|
16
|
+
this.body.setText(`${theme.queryBg(theme.white(`❯ ${query} `))}`);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { Container, Loader, type TUI } from '@mariozechner/pi-tui';
|
|
2
|
+
import type { WorkingState } from '../types.js';
|
|
3
|
+
import { getRandomThinkingVerb } from '../utils/thinking-verbs.js';
|
|
4
|
+
import { theme } from '../theme.js';
|
|
5
|
+
|
|
6
|
+
export class WorkingIndicatorComponent extends Container {
|
|
7
|
+
private readonly tui: TUI;
|
|
8
|
+
private loader: Loader | null = null;
|
|
9
|
+
private state: WorkingState = { status: 'idle' };
|
|
10
|
+
private thinkingVerb = getRandomThinkingVerb();
|
|
11
|
+
private prevStatus: WorkingState['status'] = 'idle';
|
|
12
|
+
|
|
13
|
+
constructor(tui: TUI) {
|
|
14
|
+
super();
|
|
15
|
+
this.tui = tui;
|
|
16
|
+
this.renderIdle();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
setState(state: WorkingState) {
|
|
20
|
+
const isThinking =
|
|
21
|
+
state.status === 'thinking' || state.status === 'tool' || state.status === 'approval';
|
|
22
|
+
const wasThinking =
|
|
23
|
+
this.prevStatus === 'thinking' ||
|
|
24
|
+
this.prevStatus === 'tool' ||
|
|
25
|
+
this.prevStatus === 'approval';
|
|
26
|
+
if (isThinking && !wasThinking) {
|
|
27
|
+
this.thinkingVerb = getRandomThinkingVerb();
|
|
28
|
+
}
|
|
29
|
+
this.prevStatus = state.status;
|
|
30
|
+
this.state = state;
|
|
31
|
+
if (state.status === 'idle') {
|
|
32
|
+
this.stopLoader();
|
|
33
|
+
this.renderIdle();
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
this.renderBusy();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
dispose() {
|
|
40
|
+
this.stopLoader();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
private renderIdle() {
|
|
44
|
+
this.clear();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
private renderBusy() {
|
|
48
|
+
this.clear();
|
|
49
|
+
this.ensureLoader();
|
|
50
|
+
this.updateMessage();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
private ensureLoader() {
|
|
54
|
+
if (this.loader) {
|
|
55
|
+
this.addChild(this.loader);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
this.loader = new Loader(
|
|
59
|
+
this.tui,
|
|
60
|
+
(spinner) => theme.primary(spinner),
|
|
61
|
+
(text) => theme.primary(text),
|
|
62
|
+
'',
|
|
63
|
+
);
|
|
64
|
+
// Use larger braille block characters for a more visible spinner (ora's "dots2" style)
|
|
65
|
+
(this.loader as any).frames = ['⣾', '⣽', '⣻', '⢿', '⡿', '⣟', '⣯', '⣷'];
|
|
66
|
+
this.addChild(this.loader);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
private stopLoader() {
|
|
70
|
+
if (!this.loader) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
this.loader.stop();
|
|
74
|
+
this.loader = null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
private updateMessage() {
|
|
78
|
+
if (!this.loader || this.state.status === 'idle') {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
if (this.state.status === 'approval') {
|
|
82
|
+
this.loader.setMessage('Waiting for approval... (esc to interrupt)');
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
this.loader.setMessage(`${this.thinkingVerb}... (esc to interrupt)`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import { Agent } from '../agent/agent.js';
|
|
2
|
+
import type { InMemoryChatHistory } from '../utils/in-memory-chat-history.js';
|
|
3
|
+
import type {
|
|
4
|
+
AgentConfig,
|
|
5
|
+
AgentEvent,
|
|
6
|
+
ApprovalDecision,
|
|
7
|
+
DoneEvent,
|
|
8
|
+
} from '../agent/index.js';
|
|
9
|
+
import type { DisplayEvent } from '../agent/types.js';
|
|
10
|
+
import type { HistoryItem, HistoryItemStatus, WorkingState } from '../types.js';
|
|
11
|
+
import { trackEvent } from '../utils/telemetry.js';
|
|
12
|
+
|
|
13
|
+
type ChangeListener = () => void;
|
|
14
|
+
|
|
15
|
+
export interface RunQueryResult {
|
|
16
|
+
answer: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export class AgentRunnerController {
|
|
20
|
+
private historyValue: HistoryItem[] = [];
|
|
21
|
+
private workingStateValue: WorkingState = { status: 'idle' };
|
|
22
|
+
private errorValue: string | null = null;
|
|
23
|
+
private pendingApprovalValue: { tool: string; args: Record<string, unknown> } | null = null;
|
|
24
|
+
private readonly agentConfig: AgentConfig;
|
|
25
|
+
private readonly inMemoryChatHistory: InMemoryChatHistory;
|
|
26
|
+
private readonly onChange?: ChangeListener;
|
|
27
|
+
private abortController: AbortController | null = null;
|
|
28
|
+
private approvalResolve: ((decision: ApprovalDecision) => void) | null = null;
|
|
29
|
+
private sessionApprovedTools = new Set<string>();
|
|
30
|
+
|
|
31
|
+
constructor(
|
|
32
|
+
agentConfig: AgentConfig,
|
|
33
|
+
inMemoryChatHistory: InMemoryChatHistory,
|
|
34
|
+
onChange?: ChangeListener,
|
|
35
|
+
) {
|
|
36
|
+
this.agentConfig = agentConfig;
|
|
37
|
+
this.inMemoryChatHistory = inMemoryChatHistory;
|
|
38
|
+
this.onChange = onChange;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
get history(): HistoryItem[] {
|
|
42
|
+
return this.historyValue;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
get workingState(): WorkingState {
|
|
46
|
+
return this.workingStateValue;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
get error(): string | null {
|
|
50
|
+
return this.errorValue;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
get pendingApproval(): { tool: string; args: Record<string, unknown> } | null {
|
|
54
|
+
return this.pendingApprovalValue;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
get isProcessing(): boolean {
|
|
58
|
+
return (
|
|
59
|
+
this.historyValue.length > 0 && this.historyValue[this.historyValue.length - 1]?.status === 'processing'
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
setError(error: string | null) {
|
|
64
|
+
this.errorValue = error;
|
|
65
|
+
this.emitChange();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
respondToApproval(decision: ApprovalDecision) {
|
|
69
|
+
if (!this.approvalResolve) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
this.approvalResolve(decision);
|
|
73
|
+
this.approvalResolve = null;
|
|
74
|
+
this.pendingApprovalValue = null;
|
|
75
|
+
if (decision !== 'deny') {
|
|
76
|
+
this.workingStateValue = { status: 'thinking' };
|
|
77
|
+
}
|
|
78
|
+
this.emitChange();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
cancelExecution() {
|
|
82
|
+
if (this.abortController) {
|
|
83
|
+
this.abortController.abort();
|
|
84
|
+
this.abortController = null;
|
|
85
|
+
}
|
|
86
|
+
if (this.approvalResolve) {
|
|
87
|
+
this.approvalResolve('deny');
|
|
88
|
+
this.approvalResolve = null;
|
|
89
|
+
this.pendingApprovalValue = null;
|
|
90
|
+
}
|
|
91
|
+
this.markLastProcessing('interrupted');
|
|
92
|
+
this.workingStateValue = { status: 'idle' };
|
|
93
|
+
this.emitChange();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async runQuery(query: string): Promise<RunQueryResult | undefined> {
|
|
97
|
+
this.abortController = new AbortController();
|
|
98
|
+
let finalAnswer: string | undefined;
|
|
99
|
+
|
|
100
|
+
const startTime = Date.now();
|
|
101
|
+
const item: HistoryItem = {
|
|
102
|
+
id: String(startTime),
|
|
103
|
+
query,
|
|
104
|
+
events: [],
|
|
105
|
+
answer: '',
|
|
106
|
+
status: 'processing',
|
|
107
|
+
startTime,
|
|
108
|
+
};
|
|
109
|
+
this.historyValue = [...this.historyValue, item];
|
|
110
|
+
this.inMemoryChatHistory.saveUserQuery(query);
|
|
111
|
+
this.errorValue = null;
|
|
112
|
+
this.workingStateValue = { status: 'thinking' };
|
|
113
|
+
this.emitChange();
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
trackEvent('agent_query_start', { model: this.agentConfig.model ?? '' });
|
|
117
|
+
const agent = await Agent.create({
|
|
118
|
+
...this.agentConfig,
|
|
119
|
+
signal: this.abortController.signal,
|
|
120
|
+
requestToolApproval: this.requestToolApproval,
|
|
121
|
+
sessionApprovedTools: this.sessionApprovedTools,
|
|
122
|
+
});
|
|
123
|
+
const stream = agent.run(query, this.inMemoryChatHistory);
|
|
124
|
+
for await (const event of stream) {
|
|
125
|
+
if (event.type === 'done') {
|
|
126
|
+
finalAnswer = (event as DoneEvent).answer;
|
|
127
|
+
}
|
|
128
|
+
await this.handleEvent(event);
|
|
129
|
+
}
|
|
130
|
+
if (finalAnswer) {
|
|
131
|
+
trackEvent('agent_query_complete', { model: this.agentConfig.model ?? '', duration_ms: Date.now() - startTime, success: 'true' });
|
|
132
|
+
return { answer: finalAnswer };
|
|
133
|
+
}
|
|
134
|
+
return undefined;
|
|
135
|
+
} catch (error) {
|
|
136
|
+
if (error instanceof Error && error.name === 'AbortError') {
|
|
137
|
+
trackEvent('agent_query_complete', { model: this.agentConfig.model ?? '', duration_ms: Date.now() - startTime, success: 'false', interrupted: 'true' });
|
|
138
|
+
this.markLastProcessing('interrupted');
|
|
139
|
+
this.workingStateValue = { status: 'idle' };
|
|
140
|
+
this.emitChange();
|
|
141
|
+
return undefined;
|
|
142
|
+
}
|
|
143
|
+
trackEvent('agent_query_complete', { model: this.agentConfig.model ?? '', duration_ms: Date.now() - startTime, success: 'false', interrupted: 'false' });
|
|
144
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
145
|
+
this.errorValue = message;
|
|
146
|
+
this.markLastProcessing('error');
|
|
147
|
+
this.workingStateValue = { status: 'idle' };
|
|
148
|
+
this.emitChange();
|
|
149
|
+
return undefined;
|
|
150
|
+
} finally {
|
|
151
|
+
this.abortController = null;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
private requestToolApproval = (request: { tool: string; args: Record<string, unknown> }) => {
|
|
156
|
+
return new Promise<ApprovalDecision>((resolve) => {
|
|
157
|
+
this.approvalResolve = resolve;
|
|
158
|
+
this.pendingApprovalValue = request;
|
|
159
|
+
this.workingStateValue = { status: 'approval', toolName: request.tool };
|
|
160
|
+
this.emitChange();
|
|
161
|
+
});
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
private async handleEvent(event: AgentEvent) {
|
|
165
|
+
switch (event.type) {
|
|
166
|
+
case 'thinking':
|
|
167
|
+
this.workingStateValue = { status: 'thinking' };
|
|
168
|
+
this.pushEvent({
|
|
169
|
+
id: `thinking-${Date.now()}`,
|
|
170
|
+
event,
|
|
171
|
+
completed: true,
|
|
172
|
+
});
|
|
173
|
+
break;
|
|
174
|
+
case 'tool_start': {
|
|
175
|
+
const toolId = `tool-${event.tool}-${Date.now()}`;
|
|
176
|
+
this.workingStateValue = { status: 'tool', toolName: event.tool };
|
|
177
|
+
this.updateLastItem((last) => ({
|
|
178
|
+
...last,
|
|
179
|
+
activeToolId: toolId,
|
|
180
|
+
events: [
|
|
181
|
+
...last.events,
|
|
182
|
+
{
|
|
183
|
+
id: toolId,
|
|
184
|
+
event,
|
|
185
|
+
completed: false,
|
|
186
|
+
} as DisplayEvent,
|
|
187
|
+
],
|
|
188
|
+
}));
|
|
189
|
+
break;
|
|
190
|
+
}
|
|
191
|
+
case 'tool_progress':
|
|
192
|
+
this.updateLastItem((last) => ({
|
|
193
|
+
...last,
|
|
194
|
+
events: last.events.map((entry) =>
|
|
195
|
+
entry.id === last.activeToolId ? { ...entry, progressMessage: event.message } : entry,
|
|
196
|
+
),
|
|
197
|
+
}));
|
|
198
|
+
break;
|
|
199
|
+
case 'tool_end':
|
|
200
|
+
this.finishToolEvent(event);
|
|
201
|
+
this.workingStateValue = { status: 'thinking' };
|
|
202
|
+
break;
|
|
203
|
+
case 'tool_error':
|
|
204
|
+
this.finishToolEvent(event);
|
|
205
|
+
this.workingStateValue = { status: 'thinking' };
|
|
206
|
+
break;
|
|
207
|
+
case 'tool_approval':
|
|
208
|
+
this.pushEvent({
|
|
209
|
+
id: `approval-${event.tool}-${Date.now()}`,
|
|
210
|
+
event,
|
|
211
|
+
completed: true,
|
|
212
|
+
});
|
|
213
|
+
break;
|
|
214
|
+
case 'tool_denied':
|
|
215
|
+
this.pushEvent({
|
|
216
|
+
id: `denied-${event.tool}-${Date.now()}`,
|
|
217
|
+
event,
|
|
218
|
+
completed: true,
|
|
219
|
+
});
|
|
220
|
+
break;
|
|
221
|
+
case 'tool_limit':
|
|
222
|
+
case 'context_cleared':
|
|
223
|
+
this.pushEvent({
|
|
224
|
+
id: `${event.type}-${Date.now()}`,
|
|
225
|
+
event,
|
|
226
|
+
completed: true,
|
|
227
|
+
});
|
|
228
|
+
break;
|
|
229
|
+
case 'done': {
|
|
230
|
+
const done = event as DoneEvent;
|
|
231
|
+
if (done.answer) {
|
|
232
|
+
await this.inMemoryChatHistory.saveAnswer(done.answer).catch(() => {});
|
|
233
|
+
}
|
|
234
|
+
this.updateLastItem((last) => ({
|
|
235
|
+
...last,
|
|
236
|
+
answer: done.answer,
|
|
237
|
+
status: 'complete',
|
|
238
|
+
duration: done.totalTime,
|
|
239
|
+
tokenUsage: done.tokenUsage,
|
|
240
|
+
tokensPerSecond: done.tokensPerSecond,
|
|
241
|
+
}));
|
|
242
|
+
this.workingStateValue = { status: 'idle' };
|
|
243
|
+
break;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
this.emitChange();
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
private finishToolEvent(event: AgentEvent) {
|
|
250
|
+
this.updateLastItem((last) => ({
|
|
251
|
+
...last,
|
|
252
|
+
activeToolId: undefined,
|
|
253
|
+
events: last.events.map((entry) =>
|
|
254
|
+
entry.id === last.activeToolId ? { ...entry, completed: true, endEvent: event } : entry,
|
|
255
|
+
),
|
|
256
|
+
}));
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
private pushEvent(displayEvent: DisplayEvent) {
|
|
260
|
+
this.updateLastItem((last) => ({ ...last, events: [...last.events, displayEvent] }));
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
private updateLastItem(updater: (item: HistoryItem) => HistoryItem) {
|
|
264
|
+
const last = this.historyValue[this.historyValue.length - 1];
|
|
265
|
+
if (!last || last.status !== 'processing') {
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
const next = updater(last);
|
|
269
|
+
this.historyValue = [...this.historyValue.slice(0, -1), next];
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
private markLastProcessing(status: HistoryItemStatus) {
|
|
273
|
+
const last = this.historyValue[this.historyValue.length - 1];
|
|
274
|
+
if (!last || last.status !== 'processing') {
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
this.historyValue = [...this.historyValue.slice(0, -1), { ...last, status }];
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
private emitChange() {
|
|
281
|
+
this.onChange?.();
|
|
282
|
+
}
|
|
283
|
+
}
|