opencandle 0.3.0 → 0.4.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.
Files changed (283) hide show
  1. package/assets/logo.svg +187 -0
  2. package/dist/cli.d.ts +1 -1
  3. package/dist/cli.js +38 -2
  4. package/dist/cli.js.map +1 -1
  5. package/dist/config.d.ts +9 -0
  6. package/dist/config.js +13 -0
  7. package/dist/config.js.map +1 -1
  8. package/dist/infra/browser.d.ts +10 -0
  9. package/dist/infra/browser.js +1 -0
  10. package/dist/infra/browser.js.map +1 -1
  11. package/dist/infra/native-dependencies.d.ts +1 -0
  12. package/dist/infra/native-dependencies.js +10 -0
  13. package/dist/infra/native-dependencies.js.map +1 -0
  14. package/dist/infra/node-version.d.ts +2 -0
  15. package/dist/infra/node-version.js +23 -0
  16. package/dist/infra/node-version.js.map +1 -0
  17. package/dist/memory/index.d.ts +2 -0
  18. package/dist/memory/index.js +1 -0
  19. package/dist/memory/index.js.map +1 -1
  20. package/dist/memory/sqlite.js +42 -4
  21. package/dist/memory/sqlite.js.map +1 -1
  22. package/dist/memory/storage.d.ts +6 -0
  23. package/dist/memory/storage.js +3 -3
  24. package/dist/memory/storage.js.map +1 -1
  25. package/dist/memory/tool-defaults.d.ts +8 -0
  26. package/dist/memory/tool-defaults.js +59 -0
  27. package/dist/memory/tool-defaults.js.map +1 -0
  28. package/dist/onboarding/connect.d.ts +13 -1
  29. package/dist/onboarding/connect.js +21 -10
  30. package/dist/onboarding/connect.js.map +1 -1
  31. package/dist/onboarding/prompt-user.d.ts +1 -1
  32. package/dist/onboarding/providers.d.ts +7 -0
  33. package/dist/onboarding/providers.js +6 -3
  34. package/dist/onboarding/providers.js.map +1 -1
  35. package/dist/onboarding/tool-helpers.d.ts +1 -1
  36. package/dist/pi/opencandle-extension.d.ts +7 -1
  37. package/dist/pi/opencandle-extension.js +186 -10
  38. package/dist/pi/opencandle-extension.js.map +1 -1
  39. package/dist/pi/session-storage.d.ts +2 -0
  40. package/dist/pi/session-storage.js +5 -0
  41. package/dist/pi/session-storage.js.map +1 -0
  42. package/dist/pi/session.d.ts +4 -1
  43. package/dist/pi/session.js +25 -3
  44. package/dist/pi/session.js.map +1 -1
  45. package/dist/pi/setup.d.ts +1 -1
  46. package/dist/pi/setup.js +1 -1
  47. package/dist/pi/setup.js.map +1 -1
  48. package/dist/pi/tool-adapter.d.ts +2 -2
  49. package/dist/pi/tool-adapter.js +14 -1
  50. package/dist/pi/tool-adapter.js.map +1 -1
  51. package/dist/prompts/context-builder.d.ts +22 -0
  52. package/dist/prompts/context-builder.js +45 -10
  53. package/dist/prompts/context-builder.js.map +1 -1
  54. package/dist/prompts/disclaimer.d.ts +6 -0
  55. package/dist/prompts/disclaimer.js +9 -0
  56. package/dist/prompts/disclaimer.js.map +1 -0
  57. package/dist/prompts/workflow-prompts.d.ts +8 -0
  58. package/dist/prompts/workflow-prompts.js +39 -5
  59. package/dist/prompts/workflow-prompts.js.map +1 -1
  60. package/dist/providers/yahoo-finance.js +70 -33
  61. package/dist/providers/yahoo-finance.js.map +1 -1
  62. package/dist/routing/defaults.js +1 -1
  63. package/dist/routing/defaults.js.map +1 -1
  64. package/dist/routing/index.d.ts +4 -0
  65. package/dist/routing/index.js +3 -0
  66. package/dist/routing/index.js.map +1 -1
  67. package/dist/routing/router-llm-client.d.ts +11 -0
  68. package/dist/routing/router-llm-client.js +42 -0
  69. package/dist/routing/router-llm-client.js.map +1 -0
  70. package/dist/routing/router-prompt.d.ts +2 -0
  71. package/dist/routing/router-prompt.js +138 -0
  72. package/dist/routing/router-prompt.js.map +1 -0
  73. package/dist/routing/router-types.d.ts +62 -0
  74. package/dist/routing/router-types.js +2 -0
  75. package/dist/routing/router-types.js.map +1 -0
  76. package/dist/routing/router.d.ts +10 -0
  77. package/dist/routing/router.js +194 -0
  78. package/dist/routing/router.js.map +1 -0
  79. package/dist/runtime/session-coordinator.d.ts +63 -3
  80. package/dist/runtime/session-coordinator.js +155 -4
  81. package/dist/runtime/session-coordinator.js.map +1 -1
  82. package/dist/runtime/tool-defaults-wrapper.d.ts +3 -0
  83. package/dist/runtime/tool-defaults-wrapper.js +25 -0
  84. package/dist/runtime/tool-defaults-wrapper.js.map +1 -0
  85. package/dist/sentiment/store.js +5 -0
  86. package/dist/sentiment/store.js.map +1 -1
  87. package/dist/system-prompt.js +20 -12
  88. package/dist/system-prompt.js.map +1 -1
  89. package/dist/tool-kit.d.ts +4 -4
  90. package/dist/tools/fundamentals/company-overview.d.ts +1 -1
  91. package/dist/tools/fundamentals/comps.d.ts +1 -1
  92. package/dist/tools/fundamentals/dcf.d.ts +1 -1
  93. package/dist/tools/fundamentals/earnings.d.ts +1 -1
  94. package/dist/tools/fundamentals/financials.d.ts +1 -1
  95. package/dist/tools/fundamentals/sec-filings.d.ts +1 -1
  96. package/dist/tools/index.d.ts +28 -1
  97. package/dist/tools/index.js +27 -0
  98. package/dist/tools/index.js.map +1 -1
  99. package/dist/tools/interaction/ask-user.d.ts +1 -1
  100. package/dist/tools/interaction/twitter-login.d.ts +1 -1
  101. package/dist/tools/macro/fear-greed.d.ts +1 -1
  102. package/dist/tools/macro/fred-data.d.ts +1 -1
  103. package/dist/tools/market/crypto-history.d.ts +1 -1
  104. package/dist/tools/market/crypto-price.d.ts +1 -1
  105. package/dist/tools/market/search-ticker.d.ts +1 -1
  106. package/dist/tools/market/stock-history.d.ts +1 -1
  107. package/dist/tools/market/stock-quote.d.ts +1 -1
  108. package/dist/tools/options/option-chain.d.ts +1 -1
  109. package/dist/tools/options/option-chain.js +4 -1
  110. package/dist/tools/options/option-chain.js.map +1 -1
  111. package/dist/tools/portfolio/correlation.d.ts +1 -1
  112. package/dist/tools/portfolio/predictions.d.ts +1 -1
  113. package/dist/tools/portfolio/risk-analysis.d.ts +1 -1
  114. package/dist/tools/portfolio/tracker.d.ts +1 -1
  115. package/dist/tools/portfolio/watchlist.d.ts +1 -1
  116. package/dist/tools/sentiment/reddit-sentiment.d.ts +1 -1
  117. package/dist/tools/sentiment/sentiment-summary.d.ts +1 -1
  118. package/dist/tools/sentiment/sentiment-trend.d.ts +1 -1
  119. package/dist/tools/sentiment/twitter-sentiment.d.ts +1 -1
  120. package/dist/tools/sentiment/web-search.d.ts +1 -1
  121. package/dist/tools/sentiment/web-sentiment.d.ts +1 -1
  122. package/dist/tools/technical/backtest.d.ts +1 -1
  123. package/dist/tools/technical/indicators.d.ts +1 -1
  124. package/dist/tools/technical/indicators.js +7 -1
  125. package/dist/tools/technical/indicators.js.map +1 -1
  126. package/dist/workflows/options-screener.js +7 -2
  127. package/dist/workflows/options-screener.js.map +1 -1
  128. package/dist/workflows/portfolio-builder.js +3 -3
  129. package/dist/workflows/portfolio-builder.js.map +1 -1
  130. package/gui/server/background-quotes.ts +31 -0
  131. package/gui/server/chat-event-adapter.ts +142 -0
  132. package/gui/server/invoke-tool.ts +89 -0
  133. package/gui/server/live-chat-event-adapter.ts +181 -0
  134. package/gui/server/model-setup.ts +100 -0
  135. package/gui/server/package.json +5 -0
  136. package/gui/server/projector.ts +212 -0
  137. package/gui/server/server.ts +592 -0
  138. package/gui/server/session-actions.ts +31 -0
  139. package/gui/server/tool-metadata.ts +88 -0
  140. package/gui/server/websocket.ts +128 -0
  141. package/gui/server/writer-lock.ts +118 -0
  142. package/gui/shared/chat-events.ts +118 -0
  143. package/gui/shared/event-reducer.ts +186 -0
  144. package/gui/web/dist/assets/CatalogOverlay-D1ImSJTe.js +1 -0
  145. package/gui/web/dist/assets/index-DBrWq43L.css +1 -0
  146. package/gui/web/dist/assets/index-RflHaj0y.js +67 -0
  147. package/gui/web/dist/assets/logo-CWpt6Y2a.svg +187 -0
  148. package/gui/web/dist/index.html +17 -0
  149. package/package.json +44 -18
  150. package/src/analysts/contracts.ts +189 -0
  151. package/src/analysts/orchestrator.ts +300 -0
  152. package/src/cli.ts +205 -0
  153. package/src/config.ts +161 -0
  154. package/src/index.ts +5 -0
  155. package/src/infra/browser.ts +111 -0
  156. package/src/infra/cache.ts +103 -0
  157. package/src/infra/http-client.ts +68 -0
  158. package/src/infra/index.ts +18 -0
  159. package/src/infra/native-dependencies.ts +12 -0
  160. package/src/infra/node-version.ts +24 -0
  161. package/src/infra/open-url.ts +28 -0
  162. package/src/infra/opencandle-paths.ts +64 -0
  163. package/src/infra/rate-limiter.ts +64 -0
  164. package/src/memory/index.ts +10 -0
  165. package/src/memory/manager.ts +159 -0
  166. package/src/memory/preference-extractor.ts +106 -0
  167. package/src/memory/retrieval.ts +70 -0
  168. package/src/memory/sqlite.ts +172 -0
  169. package/src/memory/storage.ts +204 -0
  170. package/src/memory/tool-defaults.ts +87 -0
  171. package/src/memory/types.ts +67 -0
  172. package/src/onboarding/connect.ts +184 -0
  173. package/src/onboarding/credential-interceptor.ts +134 -0
  174. package/src/onboarding/degradation-accumulator.ts +79 -0
  175. package/src/onboarding/prompt-user.ts +85 -0
  176. package/src/onboarding/providers.ts +315 -0
  177. package/src/onboarding/state.ts +218 -0
  178. package/src/onboarding/tool-helpers.ts +111 -0
  179. package/src/onboarding/tool-tags.ts +201 -0
  180. package/src/onboarding/validation.ts +158 -0
  181. package/src/pi/opencandle-extension.ts +724 -0
  182. package/src/pi/session-storage.ts +5 -0
  183. package/src/pi/session.ts +81 -0
  184. package/src/pi/setup.ts +371 -0
  185. package/src/pi/tool-adapter.ts +36 -0
  186. package/src/prompts/context-builder.ts +204 -0
  187. package/src/prompts/disclaimer.ts +9 -0
  188. package/src/prompts/sections.ts +46 -0
  189. package/src/prompts/workflow-prompts.ts +279 -0
  190. package/src/providers/alpha-vantage.ts +292 -0
  191. package/src/providers/coingecko.ts +96 -0
  192. package/src/providers/exa-search.ts +373 -0
  193. package/src/providers/fear-greed.ts +45 -0
  194. package/src/providers/finnhub.ts +124 -0
  195. package/src/providers/fred.ts +83 -0
  196. package/src/providers/index.ts +9 -0
  197. package/src/providers/provider-credential-error.ts +23 -0
  198. package/src/providers/reddit.ts +151 -0
  199. package/src/providers/sec-edgar.ts +96 -0
  200. package/src/providers/twitter.ts +173 -0
  201. package/src/providers/web-search.ts +293 -0
  202. package/src/providers/with-fallback.ts +41 -0
  203. package/src/providers/wrap-provider.ts +64 -0
  204. package/src/providers/yahoo-finance.ts +367 -0
  205. package/src/routing/classify-intent.ts +194 -0
  206. package/src/routing/defaults.ts +29 -0
  207. package/src/routing/entity-extractor.ts +140 -0
  208. package/src/routing/index.ts +26 -0
  209. package/src/routing/router-llm-client.ts +51 -0
  210. package/src/routing/router-prompt.ts +159 -0
  211. package/src/routing/router-types.ts +66 -0
  212. package/src/routing/router.ts +213 -0
  213. package/src/routing/slot-resolver.ts +152 -0
  214. package/src/routing/types.ts +63 -0
  215. package/src/runtime/evidence.ts +77 -0
  216. package/src/runtime/index.ts +55 -0
  217. package/src/runtime/prompt-step.ts +75 -0
  218. package/src/runtime/provider-ids.ts +15 -0
  219. package/src/runtime/provider-tracker.ts +40 -0
  220. package/src/runtime/run-context.ts +22 -0
  221. package/src/runtime/session-coordinator.ts +406 -0
  222. package/src/runtime/tool-defaults-wrapper.ts +35 -0
  223. package/src/runtime/validation.ts +214 -0
  224. package/src/runtime/workflow-events.ts +75 -0
  225. package/src/runtime/workflow-runner.ts +188 -0
  226. package/src/runtime/workflow-types.ts +102 -0
  227. package/src/sentiment/adapters/finnhub.ts +44 -0
  228. package/src/sentiment/adapters/reddit.ts +65 -0
  229. package/src/sentiment/adapters/twitter.ts +36 -0
  230. package/src/sentiment/adapters/web.ts +44 -0
  231. package/src/sentiment/index.ts +58 -0
  232. package/src/sentiment/keywords.ts +9 -0
  233. package/src/sentiment/pipeline.ts +68 -0
  234. package/src/sentiment/scorer.ts +78 -0
  235. package/src/sentiment/store.ts +260 -0
  236. package/src/sentiment/trends.ts +90 -0
  237. package/src/sentiment/types.ts +108 -0
  238. package/src/system-prompt.ts +115 -0
  239. package/src/tool-kit.ts +68 -0
  240. package/src/tools/AGENTS.md +36 -0
  241. package/src/tools/fundamentals/company-overview.ts +54 -0
  242. package/src/tools/fundamentals/comps.ts +156 -0
  243. package/src/tools/fundamentals/dcf.ts +267 -0
  244. package/src/tools/fundamentals/earnings.ts +47 -0
  245. package/src/tools/fundamentals/financials.ts +54 -0
  246. package/src/tools/fundamentals/sec-filings.ts +61 -0
  247. package/src/tools/index.ts +88 -0
  248. package/src/tools/interaction/ask-user.ts +81 -0
  249. package/src/tools/interaction/twitter-login.ts +93 -0
  250. package/src/tools/macro/fear-greed.ts +41 -0
  251. package/src/tools/macro/fred-data.ts +54 -0
  252. package/src/tools/market/crypto-history.ts +51 -0
  253. package/src/tools/market/crypto-price.ts +53 -0
  254. package/src/tools/market/search-ticker.ts +53 -0
  255. package/src/tools/market/stock-history.ts +79 -0
  256. package/src/tools/market/stock-quote.ts +64 -0
  257. package/src/tools/options/greeks.ts +82 -0
  258. package/src/tools/options/option-chain.ts +91 -0
  259. package/src/tools/portfolio/correlation.ts +162 -0
  260. package/src/tools/portfolio/predictions.ts +253 -0
  261. package/src/tools/portfolio/risk-analysis.ts +134 -0
  262. package/src/tools/portfolio/tracker.ts +147 -0
  263. package/src/tools/portfolio/watchlist.ts +153 -0
  264. package/src/tools/sentiment/reddit-sentiment.ts +164 -0
  265. package/src/tools/sentiment/sentiment-summary.ts +256 -0
  266. package/src/tools/sentiment/sentiment-trend.ts +58 -0
  267. package/src/tools/sentiment/twitter-sentiment.ts +96 -0
  268. package/src/tools/sentiment/web-search.ts +150 -0
  269. package/src/tools/sentiment/web-sentiment.ts +76 -0
  270. package/src/tools/technical/backtest.ts +246 -0
  271. package/src/tools/technical/indicators.ts +258 -0
  272. package/src/types/fundamentals.ts +46 -0
  273. package/src/types/index.ts +20 -0
  274. package/src/types/macro.ts +27 -0
  275. package/src/types/market.ts +43 -0
  276. package/src/types/options.ts +35 -0
  277. package/src/types/portfolio.ts +41 -0
  278. package/src/types/sentiment.ts +70 -0
  279. package/src/workflows/compare-assets.ts +39 -0
  280. package/src/workflows/index.ts +4 -0
  281. package/src/workflows/options-screener.ts +49 -0
  282. package/src/workflows/portfolio-builder.ts +52 -0
  283. package/src/workflows/types.ts +4 -0
@@ -0,0 +1,181 @@
1
+ import type { AgentSessionEvent } from "@earendil-works/pi-coding-agent";
2
+ import type { AssistantMessage, Message } from "@earendil-works/pi-ai";
3
+ import type { ChatEvent, MessageContent, ToolOutput } from "../shared/chat-events.js";
4
+
5
+ export interface LiveChatEventAdapterOptions {
6
+ runId: string;
7
+ sessionId: string;
8
+ startSeq: number;
9
+ emit: (event: ChatEvent) => void;
10
+ }
11
+
12
+ export interface LiveChatEventAdapter {
13
+ handle(event: AgentSessionEvent): void;
14
+ nextSeq(): number;
15
+ }
16
+
17
+ export function createLiveChatEventAdapter(options: LiveChatEventAdapterOptions): LiveChatEventAdapter {
18
+ let seq = options.startSeq;
19
+ let userCount = 0;
20
+ let assistantCount = 0;
21
+ let currentAssistantMessageId: string | undefined;
22
+ let lastAssistantMessageId: string | undefined;
23
+ const completedMessageIds = new Set<string>();
24
+
25
+ const emit = (event: Omit<ChatEvent, "seq">) => {
26
+ options.emit({ ...event, seq: seq++ } as ChatEvent);
27
+ };
28
+
29
+ const ensureAssistantMessage = (): string => {
30
+ if (currentAssistantMessageId) return currentAssistantMessageId;
31
+ currentAssistantMessageId = `${options.runId}-assistant-${++assistantCount}`;
32
+ lastAssistantMessageId = currentAssistantMessageId;
33
+ emit({ type: "message.created", messageId: currentAssistantMessageId, role: "assistant" });
34
+ return currentAssistantMessageId;
35
+ };
36
+
37
+ const messageIdForTool = (): string => lastAssistantMessageId ?? ensureAssistantMessage();
38
+
39
+ const completeAssistantMessage = (message: AssistantMessage) => {
40
+ const messageId = ensureAssistantMessage();
41
+ if (completedMessageIds.has(messageId)) return;
42
+ completedMessageIds.add(messageId);
43
+ emit({
44
+ type: "message.completed",
45
+ messageId,
46
+ content: contentToChatContent(message.content),
47
+ });
48
+ currentAssistantMessageId = undefined;
49
+ };
50
+
51
+ return {
52
+ handle(event) {
53
+ switch (event.type) {
54
+ case "message_start": {
55
+ const message = event.message as Message;
56
+ if (message.role === "user") {
57
+ const messageId = `${options.runId}-user-${++userCount}`;
58
+ emit({ type: "message.created", messageId, role: "user" });
59
+ emit({
60
+ type: "message.completed",
61
+ messageId,
62
+ content: [{ type: "text", text: messageText(message.content) }],
63
+ });
64
+ return;
65
+ }
66
+ if (message.role === "assistant") {
67
+ ensureAssistantMessage();
68
+ }
69
+ return;
70
+ }
71
+
72
+ case "message_update": {
73
+ const update = event.assistantMessageEvent;
74
+ if (update.type === "text_delta") {
75
+ emit({
76
+ type: "message.delta",
77
+ messageId: ensureAssistantMessage(),
78
+ text: update.delta,
79
+ });
80
+ }
81
+ if (update.type === "thinking_delta") {
82
+ emit({
83
+ type: "thinking.delta",
84
+ runId: options.runId,
85
+ text: update.delta,
86
+ });
87
+ }
88
+ if (update.type === "thinking_end") {
89
+ emit({
90
+ type: "thinking.completed",
91
+ runId: options.runId,
92
+ text: update.content,
93
+ });
94
+ }
95
+ return;
96
+ }
97
+
98
+ case "message_end": {
99
+ const message = event.message as Message;
100
+ if (message.role === "assistant") completeAssistantMessage(message);
101
+ return;
102
+ }
103
+
104
+ case "tool_execution_start": {
105
+ const messageId = messageIdForTool();
106
+ emit({
107
+ type: "tool.started",
108
+ toolCallId: event.toolCallId,
109
+ messageId,
110
+ name: event.toolName,
111
+ input: event.args,
112
+ });
113
+ return;
114
+ }
115
+
116
+ case "tool_execution_update":
117
+ emit({
118
+ type: "tool.delta",
119
+ toolCallId: event.toolCallId,
120
+ chunk: event.partialResult,
121
+ });
122
+ return;
123
+
124
+ case "tool_execution_end": {
125
+ const output = toolOutput(event.result, event.isError);
126
+ if (event.isError) {
127
+ emit({
128
+ type: "tool.failed",
129
+ toolCallId: event.toolCallId,
130
+ error: {
131
+ message: messageText(output.content),
132
+ details: output.details,
133
+ },
134
+ });
135
+ } else {
136
+ emit({
137
+ type: "tool.completed",
138
+ toolCallId: event.toolCallId,
139
+ output,
140
+ });
141
+ }
142
+ return;
143
+ }
144
+ }
145
+ },
146
+ nextSeq() {
147
+ return seq;
148
+ },
149
+ };
150
+ }
151
+
152
+ function contentToChatContent(content: AssistantMessage["content"]): MessageContent[] {
153
+ return content.flatMap((part): MessageContent[] => {
154
+ if (part.type === "text") return [{ type: "text", text: part.text }];
155
+ if (part.type === "toolCall") return [{ type: "tool", toolCallId: part.id }];
156
+ return [];
157
+ });
158
+ }
159
+
160
+ function messageText(content: unknown): string {
161
+ if (typeof content === "string") return content;
162
+ if (!Array.isArray(content)) return "";
163
+ return content
164
+ .map((part) => typeof part === "object" && part !== null && "text" in part && typeof part.text === "string" ? part.text : "")
165
+ .join("");
166
+ }
167
+
168
+ function toolOutput(result: unknown, isError: boolean): ToolOutput {
169
+ const record = asRecord(result);
170
+ return {
171
+ content: Array.isArray(record.content) ? record.content as ToolOutput["content"] : [],
172
+ details: record.details,
173
+ isError,
174
+ };
175
+ }
176
+
177
+ function asRecord(value: unknown): Record<string, unknown> {
178
+ return typeof value === "object" && value !== null && !Array.isArray(value)
179
+ ? value as Record<string, unknown>
180
+ : {};
181
+ }
@@ -0,0 +1,100 @@
1
+ import type { Api, Model } from "@earendil-works/pi-ai";
2
+
3
+ export type ModelSetupRequirement = "ready" | "select_model" | "connect_auth";
4
+
5
+ export interface ModelSetupProvider {
6
+ id: string;
7
+ label: string;
8
+ envVar: string;
9
+ defaultProvider: string;
10
+ defaultModel: string;
11
+ signupUrl: string;
12
+ }
13
+
14
+ export interface ModelSetupState {
15
+ requirement: ModelSetupRequirement;
16
+ currentModel?: string;
17
+ providers: ModelSetupProvider[];
18
+ availableModels: Array<{ provider: string; id: string; label: string }>;
19
+ }
20
+
21
+ export interface ModelSetupRegistry {
22
+ refresh(): void;
23
+ getAvailable(): Model<Api>[];
24
+ hasConfiguredAuth(model: Model<Api>): boolean;
25
+ }
26
+
27
+ export const modelSetupProviders: ModelSetupProvider[] = [
28
+ {
29
+ id: "google",
30
+ label: "Google Gemini",
31
+ envVar: "GEMINI_API_KEY",
32
+ defaultProvider: "google",
33
+ defaultModel: "gemini-2.5-flash",
34
+ signupUrl: "https://aistudio.google.com/app/apikey",
35
+ },
36
+ {
37
+ id: "openai",
38
+ label: "OpenAI",
39
+ envVar: "OPENAI_API_KEY",
40
+ defaultProvider: "openai",
41
+ defaultModel: "gpt-5-mini",
42
+ signupUrl: "https://platform.openai.com/api-keys",
43
+ },
44
+ {
45
+ id: "anthropic",
46
+ label: "Anthropic",
47
+ envVar: "ANTHROPIC_API_KEY",
48
+ defaultProvider: "anthropic",
49
+ defaultModel: "claude-haiku-4-5",
50
+ signupUrl: "https://console.anthropic.com/settings/keys",
51
+ },
52
+ ];
53
+
54
+ export function buildModelSetupState(
55
+ registry: ModelSetupRegistry,
56
+ currentModel: Model<Api> | undefined,
57
+ ): ModelSetupState {
58
+ registry.refresh();
59
+ const availableModels = sortModels(registry.getAvailable()).map((model) => ({
60
+ provider: model.provider,
61
+ id: model.id,
62
+ label: `${model.provider}/${model.id}`,
63
+ }));
64
+ const requirement =
65
+ currentModel && registry.hasConfiguredAuth(currentModel)
66
+ ? "ready"
67
+ : availableModels.length > 0
68
+ ? "select_model"
69
+ : "connect_auth";
70
+
71
+ return {
72
+ requirement,
73
+ currentModel: currentModel ? `${currentModel.provider}/${currentModel.id}` : undefined,
74
+ providers: modelSetupProviders,
75
+ availableModels,
76
+ };
77
+ }
78
+
79
+ export function findPreferredModel(
80
+ registry: Pick<ModelSetupRegistry, "getAvailable">,
81
+ provider: ModelSetupProvider,
82
+ ): Model<Api> | undefined {
83
+ const available = sortModels(registry.getAvailable(), provider.defaultProvider);
84
+ return (
85
+ available.find(
86
+ (model) =>
87
+ model.provider === provider.defaultProvider && model.id === provider.defaultModel,
88
+ ) ?? available.find((model) => model.provider === provider.defaultProvider)
89
+ );
90
+ }
91
+
92
+ export function sortModels(models: Model<Api>[], preferredProvider?: string): Model<Api>[] {
93
+ return [...models].sort((a, b) => {
94
+ const aPreferred = preferredProvider && a.provider === preferredProvider ? -1 : 0;
95
+ const bPreferred = preferredProvider && b.provider === preferredProvider ? -1 : 0;
96
+ if (aPreferred !== bPreferred) return aPreferred - bPreferred;
97
+ const byProvider = a.provider.localeCompare(b.provider);
98
+ return byProvider !== 0 ? byProvider : a.id.localeCompare(b.id);
99
+ });
100
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "name": "@opencandle/gui-server",
3
+ "private": true,
4
+ "type": "module"
5
+ }
@@ -0,0 +1,212 @@
1
+ import type { SessionEntry } from "@earendil-works/pi-coding-agent";
2
+ import type { Message, ToolResultMessage } from "@earendil-works/pi-ai";
3
+
4
+ export interface DashboardState {
5
+ watchlist: Array<{
6
+ symbol: string;
7
+ quote: Record<string, unknown> | null;
8
+ pinned: boolean;
9
+ lastSeen: string;
10
+ }>;
11
+ activeAnalyses: Array<{
12
+ workflowId: string;
13
+ workflow: string;
14
+ symbol?: string;
15
+ analystsTotal: number;
16
+ analystsDone: number;
17
+ startedAt: string;
18
+ }>;
19
+ recentResearch: Array<{
20
+ sessionId: string;
21
+ workflow: string;
22
+ symbol?: string;
23
+ completedAt: string;
24
+ }>;
25
+ dataQuality: {
26
+ softGaps: Array<{ provider: string; lastSeen: string }>;
27
+ hardSkips: Array<{ provider: string; lastSeen: string }>;
28
+ };
29
+ }
30
+
31
+ export function createEmptyDashboardState(): DashboardState {
32
+ return {
33
+ watchlist: [],
34
+ activeAnalyses: [],
35
+ recentResearch: [],
36
+ dataQuality: { softGaps: [], hardSkips: [] },
37
+ };
38
+ }
39
+
40
+ export function projectDashboard(entries: SessionEntry[], sessionId = "local"): DashboardState {
41
+ const state = createEmptyDashboardState();
42
+
43
+ for (const entry of entries) {
44
+ if (entry.type === "message") {
45
+ projectMessage(state, entry.message as Message, entry.timestamp, sessionId);
46
+ continue;
47
+ }
48
+
49
+ if (entry.type === "custom" && entry.customType === "opencandle-workflow") {
50
+ const data = asRecord(entry.data);
51
+ const workflow = stringValue(data.workflow) ?? stringValue(data.workflowType) ?? "workflow";
52
+ const slots = asRecord(data.resolvedSlots);
53
+ const symbol = stringValue(slots.symbol) ?? firstString(slots.symbols);
54
+ state.activeAnalyses.push({
55
+ workflowId: stringValue(data.runId) ?? entry.id,
56
+ workflow,
57
+ symbol,
58
+ analystsTotal: numberValue(data.analystsTotal) ?? 0,
59
+ analystsDone: 0,
60
+ startedAt: entry.timestamp,
61
+ });
62
+ continue;
63
+ }
64
+
65
+ if (entry.type === "custom" && entry.customType === "opencandle-turn-gap") {
66
+ // The accumulator writes a single combined annotation string with one
67
+ // [OPENCANDLE_SKIPPED ... provider=X ...] tag per fallback provider.
68
+ // Older shapes may carry { softGaps: [...] } or { provider } directly —
69
+ // accept either to stay forward/backward compatible.
70
+ const data = asRecord(entry.data);
71
+ const annotation = stringValue(data.annotation);
72
+ if (annotation) {
73
+ for (const provider of parseSkippedProviders(annotation)) {
74
+ state.dataQuality.softGaps.push({ provider, lastSeen: entry.timestamp });
75
+ }
76
+ }
77
+ for (const gap of asArray(data.softGaps)) {
78
+ const provider = stringValue(asRecord(gap).provider);
79
+ if (provider) state.dataQuality.softGaps.push({ provider, lastSeen: entry.timestamp });
80
+ }
81
+ const provider = stringValue(data.provider);
82
+ if (provider) state.dataQuality.softGaps.push({ provider, lastSeen: entry.timestamp });
83
+ }
84
+
85
+ if (entry.type === "custom" && entry.customType === "opencandle-quote-refresh") {
86
+ const data = asRecord(entry.data);
87
+ projectQuote(
88
+ state,
89
+ stringValue(data.symbol),
90
+ asRecord(data.value),
91
+ asArray(data.content) as ToolResultMessage["content"],
92
+ entry.timestamp,
93
+ );
94
+ }
95
+ }
96
+
97
+ return state;
98
+ }
99
+
100
+ function projectMessage(
101
+ state: DashboardState,
102
+ message: Message,
103
+ timestamp: string,
104
+ sessionId: string,
105
+ ): void {
106
+ if (message.role === "toolResult") {
107
+ projectToolResult(state, message, timestamp);
108
+ return;
109
+ }
110
+
111
+ if (message.role === "assistant" && message.stopReason === "stop") {
112
+ const active = state.activeAnalyses.shift();
113
+ if (active) {
114
+ state.recentResearch.unshift({
115
+ sessionId,
116
+ workflow: active.workflow,
117
+ symbol: active.symbol,
118
+ completedAt: timestamp,
119
+ });
120
+ }
121
+ }
122
+ }
123
+
124
+ function projectToolResult(
125
+ state: DashboardState,
126
+ message: ToolResultMessage,
127
+ timestamp: string,
128
+ ): void {
129
+ if (message.toolName === "get_stock_quote") {
130
+ const rawDetails = asRecord(message.details);
131
+ const nestedValue = asRecord(rawDetails.value);
132
+ const details = Object.keys(nestedValue).length > 0 ? nestedValue : rawDetails;
133
+ projectQuote(state, stringValue(details.symbol), details, message.content, timestamp);
134
+ }
135
+
136
+ const text = message.content
137
+ .filter((part) => part.type === "text")
138
+ .map((part) => part.text)
139
+ .join("\n");
140
+ for (const provider of parseCredentialRequiredProviders(text)) {
141
+ state.dataQuality.hardSkips.push({ provider, lastSeen: timestamp });
142
+ }
143
+ }
144
+
145
+ function projectQuote(
146
+ state: DashboardState,
147
+ symbolHint: string | undefined,
148
+ details: Record<string, unknown>,
149
+ content: ToolResultMessage["content"],
150
+ timestamp: string,
151
+ ): void {
152
+ const symbol = symbolHint ?? inferSymbolFromContent(content);
153
+ if (!symbol) return;
154
+
155
+ const existing = state.watchlist.find((row) => row.symbol === symbol);
156
+ const row = {
157
+ symbol,
158
+ quote: Object.keys(details).length > 0 ? details : null,
159
+ pinned: existing?.pinned ?? false,
160
+ lastSeen: timestamp,
161
+ };
162
+ if (existing) Object.assign(existing, row);
163
+ else state.watchlist.push(row);
164
+ }
165
+
166
+ function parseCredentialRequiredProviders(text: string): string[] {
167
+ const providers: string[] = [];
168
+ const re = /\[OPENCANDLE_CREDENTIAL_REQUIRED[^\]]*provider=([a-z0-9_-]+)/gi;
169
+ let match: RegExpExecArray | null;
170
+ while ((match = re.exec(text)) !== null) {
171
+ providers.push(match[1]);
172
+ }
173
+ return providers;
174
+ }
175
+
176
+ function parseSkippedProviders(text: string): string[] {
177
+ const providers: string[] = [];
178
+ const re = /\[OPENCANDLE_SKIPPED[^\]]*provider=([a-z0-9_-]+)/gi;
179
+ let match: RegExpExecArray | null;
180
+ while ((match = re.exec(text)) !== null) {
181
+ providers.push(match[1]);
182
+ }
183
+ return providers;
184
+ }
185
+
186
+ function inferSymbolFromContent(content: ToolResultMessage["content"]): string | undefined {
187
+ const text = content.find((part) => part.type === "text")?.text;
188
+ const match = text?.match(/^([A-Z]{1,8})\b/);
189
+ return match?.[1];
190
+ }
191
+
192
+ function asRecord(value: unknown): Record<string, unknown> {
193
+ return typeof value === "object" && value !== null && !Array.isArray(value)
194
+ ? value as Record<string, unknown>
195
+ : {};
196
+ }
197
+
198
+ function asArray(value: unknown): unknown[] {
199
+ return Array.isArray(value) ? value : [];
200
+ }
201
+
202
+ function stringValue(value: unknown): string | undefined {
203
+ return typeof value === "string" && value.length > 0 ? value : undefined;
204
+ }
205
+
206
+ function firstString(value: unknown): string | undefined {
207
+ return Array.isArray(value) ? value.find((item) => typeof item === "string") : undefined;
208
+ }
209
+
210
+ function numberValue(value: unknown): number | undefined {
211
+ return typeof value === "number" ? value : undefined;
212
+ }