opencandle 0.6.0 → 0.8.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/README.md +43 -23
- package/dist/cli-main.d.ts +1 -0
- package/dist/cli-main.js +200 -0
- package/dist/cli-main.js.map +1 -0
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +14 -194
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +10 -0
- package/dist/config.js +13 -0
- package/dist/config.js.map +1 -1
- package/dist/doctor/cli-command.d.ts +1 -0
- package/dist/doctor/cli-command.js +69 -0
- package/dist/doctor/cli-command.js.map +1 -0
- package/dist/doctor/render.d.ts +2 -0
- package/dist/doctor/render.js +32 -0
- package/dist/doctor/render.js.map +1 -0
- package/dist/doctor/report.d.ts +63 -0
- package/dist/doctor/report.js +500 -0
- package/dist/doctor/report.js.map +1 -0
- package/dist/infra/index.d.ts +0 -1
- package/dist/infra/index.js +0 -1
- package/dist/infra/index.js.map +1 -1
- package/dist/infra/native-dependencies.d.ts +8 -0
- package/dist/infra/native-dependencies.js +49 -0
- package/dist/infra/native-dependencies.js.map +1 -1
- package/dist/infra/node-version.js +2 -5
- package/dist/infra/node-version.js.map +1 -1
- package/dist/monitor.d.ts +1 -1
- package/dist/monitor.js +2 -1
- package/dist/monitor.js.map +1 -1
- package/dist/onboarding/connect.d.ts +2 -2
- package/dist/onboarding/connect.js +10 -3
- package/dist/onboarding/connect.js.map +1 -1
- package/dist/onboarding/provider-status.d.ts +46 -0
- package/dist/onboarding/provider-status.js +256 -0
- package/dist/onboarding/provider-status.js.map +1 -0
- package/dist/onboarding/providers.d.ts +85 -8
- package/dist/onboarding/providers.js +87 -9
- package/dist/onboarding/providers.js.map +1 -1
- package/dist/onboarding/state.d.ts +1 -0
- package/dist/onboarding/state.js +5 -0
- package/dist/onboarding/state.js.map +1 -1
- package/dist/onboarding/tool-tags.d.ts +12 -1
- package/dist/onboarding/tool-tags.js +31 -1
- package/dist/onboarding/tool-tags.js.map +1 -1
- package/dist/onboarding/validation.d.ts +2 -2
- package/dist/onboarding/validation.js.map +1 -1
- package/dist/pi/opencandle-extension.js +91 -15
- package/dist/pi/opencandle-extension.js.map +1 -1
- package/dist/pi/session.d.ts +0 -1
- package/dist/pi/session.js +2 -1
- package/dist/pi/session.js.map +1 -1
- package/dist/pi/tool-adapter.d.ts +4 -1
- package/dist/pi/tool-adapter.js +5 -4
- package/dist/pi/tool-adapter.js.map +1 -1
- package/dist/prompts/context-builder.js +1 -1
- package/dist/prompts/policy-cards.js +9 -5
- package/dist/prompts/policy-cards.js.map +1 -1
- package/dist/prompts/workflow-prompts.js +10 -6
- package/dist/prompts/workflow-prompts.js.map +1 -1
- package/dist/providers/external-tool-command.d.ts +17 -0
- package/dist/providers/external-tool-command.js +114 -0
- package/dist/providers/external-tool-command.js.map +1 -0
- package/dist/providers/external-tool-error.d.ts +10 -0
- package/dist/providers/external-tool-error.js +21 -0
- package/dist/providers/external-tool-error.js.map +1 -0
- package/dist/providers/reddit-cli.d.ts +32 -0
- package/dist/providers/reddit-cli.js +172 -0
- package/dist/providers/reddit-cli.js.map +1 -0
- package/dist/providers/reddit.d.ts +1 -1
- package/dist/providers/reddit.js +7 -35
- package/dist/providers/reddit.js.map +1 -1
- package/dist/providers/twitter-cli.d.ts +36 -0
- package/dist/providers/twitter-cli.js +124 -0
- package/dist/providers/twitter-cli.js.map +1 -0
- package/dist/providers/twitter.d.ts +0 -8
- package/dist/providers/twitter.js +4 -54
- package/dist/providers/twitter.js.map +1 -1
- package/dist/providers/wrap-provider.js +30 -0
- package/dist/providers/wrap-provider.js.map +1 -1
- package/dist/providers/yahoo-finance.js +53 -32
- package/dist/providers/yahoo-finance.js.map +1 -1
- package/dist/routing/classify-intent.js +7 -3
- package/dist/routing/classify-intent.js.map +1 -1
- package/dist/routing/planning.d.ts +1 -1
- package/dist/routing/planning.js +9 -6
- package/dist/routing/planning.js.map +1 -1
- package/dist/routing/router.js +7 -3
- package/dist/routing/router.js.map +1 -1
- package/dist/runtime/answer-contracts.d.ts +1 -1
- package/dist/runtime/answer-contracts.js +12 -1
- package/dist/runtime/answer-contracts.js.map +1 -1
- package/dist/runtime/tool-defaults-wrapper.js +6 -2
- package/dist/runtime/tool-defaults-wrapper.js.map +1 -1
- package/dist/sentiment/index.d.ts +1 -0
- package/dist/sentiment/index.js +1 -0
- package/dist/sentiment/index.js.map +1 -1
- package/dist/sentiment/insights.d.ts +17 -0
- package/dist/sentiment/insights.js +206 -0
- package/dist/sentiment/insights.js.map +1 -0
- package/dist/sentiment/pipeline.js +13 -1
- package/dist/sentiment/pipeline.js.map +1 -1
- package/dist/sentiment/scorer.d.ts +2 -0
- package/dist/sentiment/scorer.js +10 -1
- package/dist/sentiment/scorer.js.map +1 -1
- package/dist/sentiment/types.d.ts +2 -0
- package/dist/sentiment/types.js.map +1 -1
- package/dist/system-prompt.js +3 -7
- package/dist/system-prompt.js.map +1 -1
- package/dist/tools/index.d.ts +5 -2
- package/dist/tools/index.js +8 -8
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/sentiment/insight-format.d.ts +2 -0
- package/dist/tools/sentiment/insight-format.js +36 -0
- package/dist/tools/sentiment/insight-format.js.map +1 -0
- package/dist/tools/sentiment/query-match.d.ts +3 -0
- package/dist/tools/sentiment/query-match.js +113 -0
- package/dist/tools/sentiment/query-match.js.map +1 -0
- package/dist/tools/sentiment/reddit-sentiment.d.ts +12 -1
- package/dist/tools/sentiment/reddit-sentiment.js +263 -117
- package/dist/tools/sentiment/reddit-sentiment.js.map +1 -1
- package/dist/tools/sentiment/sentiment-summary.d.ts +9 -1
- package/dist/tools/sentiment/sentiment-summary.js +217 -201
- package/dist/tools/sentiment/sentiment-summary.js.map +1 -1
- package/dist/tools/sentiment/twitter-sentiment.d.ts +11 -1
- package/dist/tools/sentiment/twitter-sentiment.js +187 -64
- package/dist/tools/sentiment/twitter-sentiment.js.map +1 -1
- package/dist/tools/sentiment/web-sentiment.js +4 -0
- package/dist/tools/sentiment/web-sentiment.js.map +1 -1
- package/dist/types/sentiment.d.ts +52 -0
- package/gui/server/http-routes.ts +419 -0
- package/gui/server/invoke-tool.ts +17 -3
- package/gui/server/model-setup.ts +10 -3
- package/gui/server/projector.ts +6 -2
- package/gui/server/server.ts +30 -275
- package/gui/server/tool-metadata.ts +80 -16
- package/gui/server/ws-hub.ts +19 -0
- package/gui/web/dist/assets/CatalogOverlay-CCVKwBUB.js +1 -0
- package/gui/web/dist/assets/index-Dm4Aom2_.js +69 -0
- package/gui/web/dist/assets/{index-2KZtKBmu.css → index-UzZUg3dx.css} +1 -1
- package/gui/web/dist/index.html +2 -2
- package/package.json +17 -14
- package/src/cli-main.ts +233 -0
- package/src/cli.ts +14 -226
- package/src/config.ts +27 -0
- package/src/doctor/cli-command.ts +83 -0
- package/src/doctor/render.ts +37 -0
- package/src/doctor/report.ts +638 -0
- package/src/infra/index.ts +0 -1
- package/src/infra/native-dependencies.ts +67 -0
- package/src/infra/node-version.ts +2 -5
- package/src/monitor.ts +3 -1
- package/src/onboarding/connect.ts +20 -4
- package/src/onboarding/provider-status.ts +382 -0
- package/src/onboarding/providers.ts +148 -18
- package/src/onboarding/state.ts +9 -0
- package/src/onboarding/tool-tags.ts +45 -2
- package/src/onboarding/validation.ts +2 -2
- package/src/pi/opencandle-extension.ts +115 -17
- package/src/pi/session.ts +2 -1
- package/src/pi/tool-adapter.ts +14 -4
- package/src/prompts/context-builder.ts +1 -1
- package/src/prompts/policy-cards.ts +13 -5
- package/src/prompts/workflow-prompts.ts +13 -6
- package/src/providers/external-tool-command.ts +164 -0
- package/src/providers/external-tool-error.ts +20 -0
- package/src/providers/reddit-cli.ts +286 -0
- package/src/providers/reddit.ts +7 -63
- package/src/providers/twitter-cli.ts +202 -0
- package/src/providers/twitter.ts +4 -73
- package/src/providers/wrap-provider.ts +34 -0
- package/src/providers/yahoo-finance.ts +65 -32
- package/src/routing/classify-intent.ts +11 -6
- package/src/routing/planning.ts +16 -10
- package/src/routing/router.ts +11 -6
- package/src/runtime/answer-contracts.ts +23 -2
- package/src/runtime/tool-defaults-wrapper.ts +12 -2
- package/src/sentiment/index.ts +1 -0
- package/src/sentiment/insights.ts +269 -0
- package/src/sentiment/pipeline.ts +13 -1
- package/src/sentiment/scorer.ts +12 -1
- package/src/sentiment/types.ts +3 -0
- package/src/system-prompt.ts +3 -7
- package/src/tools/index.ts +9 -8
- package/src/tools/sentiment/insight-format.ts +50 -0
- package/src/tools/sentiment/query-match.ts +117 -0
- package/src/tools/sentiment/reddit-sentiment.ts +354 -141
- package/src/tools/sentiment/sentiment-summary.ts +283 -237
- package/src/tools/sentiment/twitter-sentiment.ts +262 -78
- package/src/tools/sentiment/web-sentiment.ts +4 -0
- package/src/types/sentiment.ts +59 -0
- package/dist/infra/browser.d.ts +0 -35
- package/dist/infra/browser.js +0 -105
- package/dist/infra/browser.js.map +0 -1
- package/dist/tools/interaction/twitter-login.d.ts +0 -8
- package/dist/tools/interaction/twitter-login.js +0 -87
- package/dist/tools/interaction/twitter-login.js.map +0 -1
- package/gui/web/dist/assets/CatalogOverlay-eJ2cBk33.js +0 -1
- package/gui/web/dist/assets/index-CveNgtDg.js +0 -69
- package/src/infra/browser.ts +0 -113
- package/src/tools/interaction/twitter-login.ts +0 -105
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
import { createReadStream, existsSync } from "node:fs";
|
|
2
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
3
|
+
import { extname, join, resolve } from "node:path";
|
|
4
|
+
import { type AgentSession, SessionManager } from "@earendil-works/pi-coding-agent";
|
|
5
|
+
import { buildDoctorReport } from "../../src/doctor/report.js";
|
|
6
|
+
import { probeProviderStatus } from "../../src/onboarding/provider-status.js";
|
|
7
|
+
import type { ChatEvent } from "../shared/chat-events.js";
|
|
8
|
+
import { sessionEntriesToChatEvents } from "./chat-event-adapter.js";
|
|
9
|
+
import { chatRunSessionConflict } from "./chat-run-session.js";
|
|
10
|
+
import { createLiveChatEventAdapter } from "./live-chat-event-adapter.js";
|
|
11
|
+
import { buildMarketStateSnapshot, searchInstrumentCandidates } from "./market-state-api.js";
|
|
12
|
+
import { buildModelSetupState, type ModelSetupController } from "./model-setup.js";
|
|
13
|
+
import { isTrustedPrivateApiRequest, privateApiCookieHeader } from "./private-api-access.js";
|
|
14
|
+
import { createPromptObservation, observePromptEvent } from "./prompt-observation.js";
|
|
15
|
+
import type { QuoteSnapshotStore } from "./quote-snapshot-store.js";
|
|
16
|
+
import { promptAndSettle, type SessionActionsController } from "./session-actions.js";
|
|
17
|
+
import { waitForNewEntryId } from "./session-entry-wait.js";
|
|
18
|
+
import { buildCatalog } from "./tool-metadata.js";
|
|
19
|
+
import type { WsHub } from "./ws-hub.js";
|
|
20
|
+
|
|
21
|
+
interface GuiHttpRouteOptions {
|
|
22
|
+
host: string;
|
|
23
|
+
port: number;
|
|
24
|
+
webDist: string;
|
|
25
|
+
role: string;
|
|
26
|
+
cwd: string;
|
|
27
|
+
agentDir: string;
|
|
28
|
+
sessionDir: string;
|
|
29
|
+
privateApiSessionToken: string;
|
|
30
|
+
allowRemotePrivateApi: boolean;
|
|
31
|
+
getSession: () => AgentSession;
|
|
32
|
+
getSessionManager: () => SessionManager;
|
|
33
|
+
wsHub: WsHub;
|
|
34
|
+
modelSetupController: ModelSetupController;
|
|
35
|
+
sessionActionsController: SessionActionsController;
|
|
36
|
+
quoteSnapshotStore: QuoteSnapshotStore;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function createHttpRequestHandler(options: GuiHttpRouteOptions) {
|
|
40
|
+
return async function handleHttpRequest(
|
|
41
|
+
req: IncomingMessage,
|
|
42
|
+
res: ServerResponse,
|
|
43
|
+
): Promise<void> {
|
|
44
|
+
const url = new URL(req.url ?? "/", `http://${options.host}:${options.port}`);
|
|
45
|
+
if (url.pathname === "/health") {
|
|
46
|
+
writeJson(res, { ok: true, role: options.role });
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (url.pathname === "/api/bootstrap" && req.method === "GET") {
|
|
51
|
+
if (!allowTrustedGuiRequest(req, res, "Bootstrap API", options)) return;
|
|
52
|
+
writeJson(res, await options.wsHub.buildBootstrapPayload());
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (url.pathname === "/api/session/new" && req.method === "POST") {
|
|
57
|
+
if (!allowTrustedGuiRequest(req, res, "Session API", options)) return;
|
|
58
|
+
if (options.role !== "writer") {
|
|
59
|
+
writeJson(res, { error: "Read-only follower mode" }, 409);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
await options.sessionActionsController.handleNewSession();
|
|
63
|
+
options.wsHub.broadcastState();
|
|
64
|
+
options.wsHub.broadcastSessions();
|
|
65
|
+
writeJson(res, await options.wsHub.buildBootstrapPayload());
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (url.pathname === "/api/sessions" && req.method === "GET") {
|
|
70
|
+
if (!allowTrustedGuiRequest(req, res, "Session API", options)) return;
|
|
71
|
+
writeJson(res, {
|
|
72
|
+
currentSessionId: options.getSessionManager().getSessionId(),
|
|
73
|
+
role: options.role,
|
|
74
|
+
sessions: await SessionManager.list(options.cwd, options.sessionDir),
|
|
75
|
+
});
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (url.pathname === "/api/session/events" && req.method === "GET") {
|
|
80
|
+
if (!allowTrustedGuiRequest(req, res, "Session API", options)) return;
|
|
81
|
+
writeJson(res, {
|
|
82
|
+
sessionId: options.getSessionManager().getSessionId(),
|
|
83
|
+
role: options.role,
|
|
84
|
+
events: options.wsHub.currentChatEvents(),
|
|
85
|
+
});
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (url.pathname === "/api/model-setup/refresh" && req.method === "POST") {
|
|
90
|
+
if (!allowTrustedGuiRequest(req, res, "Model setup API", options)) return;
|
|
91
|
+
options.getSession().modelRegistry.refresh();
|
|
92
|
+
options.wsHub.broadcastModelSetup();
|
|
93
|
+
writeJson(res, await options.wsHub.buildBootstrapPayload());
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (url.pathname === "/api/model-setup/api-key" && req.method === "POST") {
|
|
98
|
+
if (!allowTrustedGuiRequest(req, res, "Model setup API", options)) return;
|
|
99
|
+
await handleTrustedGuiMutation(req, res, options, async (body) => {
|
|
100
|
+
await options.modelSetupController.handleSaveModelApiKey(
|
|
101
|
+
String(body.provider ?? ""),
|
|
102
|
+
String(body.apiKey ?? ""),
|
|
103
|
+
);
|
|
104
|
+
options.wsHub.broadcastModelSetup();
|
|
105
|
+
});
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (url.pathname === "/api/model-setup/model" && req.method === "POST") {
|
|
110
|
+
if (!allowTrustedGuiRequest(req, res, "Model setup API", options)) return;
|
|
111
|
+
await handleTrustedGuiMutation(req, res, options, async (body) => {
|
|
112
|
+
await options.modelSetupController.handleSelectModel(
|
|
113
|
+
String(body.provider ?? ""),
|
|
114
|
+
String(body.modelId ?? ""),
|
|
115
|
+
);
|
|
116
|
+
options.wsHub.broadcastModelSetup();
|
|
117
|
+
});
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (url.pathname === "/api/provider-setup/api-key" && req.method === "POST") {
|
|
122
|
+
if (!allowTrustedGuiRequest(req, res, "Provider setup API", options)) return;
|
|
123
|
+
await handleTrustedGuiMutation(req, res, options, async (body) => {
|
|
124
|
+
await options.modelSetupController.handleSaveProviderApiKey(
|
|
125
|
+
String(body.providerId ?? ""),
|
|
126
|
+
String(body.apiKey ?? ""),
|
|
127
|
+
);
|
|
128
|
+
options.wsHub.broadcast({ type: "catalog", catalog: buildCatalog() });
|
|
129
|
+
});
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (url.pathname === "/api/market-state" && req.method === "GET") {
|
|
134
|
+
if (!allowTrustedGuiRequest(req, res, "Market-state API", options)) return;
|
|
135
|
+
writeJson(res, buildMarketStateSnapshot());
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (url.pathname === "/api/market-state/quotes" && req.method === "GET") {
|
|
140
|
+
if (!allowTrustedGuiRequest(req, res, "Market-state API", options)) return;
|
|
141
|
+
writeJson(res, await options.quoteSnapshotStore.get());
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (url.pathname === "/api/doctor" && req.method === "GET") {
|
|
146
|
+
if (!allowTrustedGuiRequest(req, res, "Diagnostics API", options)) return;
|
|
147
|
+
const session = options.getSession();
|
|
148
|
+
writeJson(
|
|
149
|
+
res,
|
|
150
|
+
await buildDoctorReport({
|
|
151
|
+
cwd: options.cwd,
|
|
152
|
+
agentDir: options.agentDir,
|
|
153
|
+
includeSessions: url.searchParams.get("sessions") === "1",
|
|
154
|
+
gui: {
|
|
155
|
+
host: options.host,
|
|
156
|
+
port: options.port,
|
|
157
|
+
role: options.role,
|
|
158
|
+
reachable: true,
|
|
159
|
+
healthEndpoint: `http://${options.host}:${options.port}/health`,
|
|
160
|
+
},
|
|
161
|
+
modelSetup: buildModelSetupState(session.modelRegistry, session.model),
|
|
162
|
+
}),
|
|
163
|
+
);
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (url.pathname === "/api/instruments/search" && req.method === "GET") {
|
|
168
|
+
if (!allowTrustedGuiRequest(req, res, "Market-state API", options)) return;
|
|
169
|
+
writeJson(res, await searchInstrumentCandidates(url.searchParams.get("q") ?? ""));
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (url.pathname === "/api/diagnostics/twitter-cli" && req.method === "GET") {
|
|
174
|
+
if (!allowTrustedGuiRequest(req, res, "Diagnostics API", options)) return;
|
|
175
|
+
const mode = url.searchParams.get("mode") === "session" ? "session" : "install";
|
|
176
|
+
const force = url.searchParams.get("force") === "1";
|
|
177
|
+
writeJson(res, await probeProviderStatus("twitter", { mode, force }));
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (url.pathname === "/api/diagnostics/reddit-cli" && req.method === "GET") {
|
|
182
|
+
if (!allowTrustedGuiRequest(req, res, "Diagnostics API", options)) return;
|
|
183
|
+
const mode = url.searchParams.get("mode") === "session" ? "session" : "install";
|
|
184
|
+
const force = url.searchParams.get("force") === "1";
|
|
185
|
+
writeJson(res, await probeProviderStatus("reddit", { mode, force }));
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (url.pathname === "/api/chat/run" && req.method === "POST") {
|
|
190
|
+
if (!allowTrustedGuiRequest(req, res, "Chat run API", options)) return;
|
|
191
|
+
await handleSseChatRun(req, res, options);
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
serveStaticAsset(url.pathname, res, options);
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
async function handleSseChatRun(
|
|
200
|
+
req: IncomingMessage,
|
|
201
|
+
res: ServerResponse,
|
|
202
|
+
options: GuiHttpRouteOptions,
|
|
203
|
+
): Promise<void> {
|
|
204
|
+
if (options.role !== "writer") {
|
|
205
|
+
writeJson(res, { error: "Read-only follower mode" }, 409);
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const body = await readJsonBody(req);
|
|
210
|
+
const prompt = String(asRecord(body).prompt ?? "").trim();
|
|
211
|
+
if (!prompt) {
|
|
212
|
+
writeJson(res, { error: "prompt is required" }, 400);
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const sessionManager = options.getSessionManager();
|
|
217
|
+
const sessionConflict = chatRunSessionConflict(
|
|
218
|
+
asRecord(body).sessionId,
|
|
219
|
+
sessionManager.getSessionId(),
|
|
220
|
+
);
|
|
221
|
+
if (sessionConflict) {
|
|
222
|
+
writeJson(res, sessionConflict, 409);
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
res.writeHead(200, {
|
|
227
|
+
"content-type": "text/event-stream; charset=utf-8",
|
|
228
|
+
"cache-control": "no-cache, no-transform",
|
|
229
|
+
connection: "keep-alive",
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
let seq = 1;
|
|
233
|
+
const runId = `gui-run-${Date.now()}`;
|
|
234
|
+
const runSession = options.getSession();
|
|
235
|
+
const runSessionManager = options.getSessionManager();
|
|
236
|
+
const sessionId = runSessionManager.getSessionId();
|
|
237
|
+
if (!prompt.startsWith("/") && !runSessionManager.getSessionName()) {
|
|
238
|
+
runSessionManager.appendSessionInfo(prompt.length > 80 ? `${prompt.slice(0, 77)}...` : prompt);
|
|
239
|
+
}
|
|
240
|
+
const beforeEntries = runSessionManager.getEntries();
|
|
241
|
+
const beforeCount = beforeEntries.length;
|
|
242
|
+
const beforeIds = new Set(beforeEntries.map((entry) => entry.id));
|
|
243
|
+
writeSse(res, { type: "run.started", runId, sessionId, seq: seq++ });
|
|
244
|
+
res.flushHeaders?.();
|
|
245
|
+
const liveStartSeq = seq;
|
|
246
|
+
const liveAdapter = createLiveChatEventAdapter({
|
|
247
|
+
runId,
|
|
248
|
+
sessionId,
|
|
249
|
+
startSeq: seq,
|
|
250
|
+
emit: (event) => writeSse(res, event),
|
|
251
|
+
originalPrompt: prompt,
|
|
252
|
+
});
|
|
253
|
+
const observation = createPromptObservation();
|
|
254
|
+
const unsubscribeLive = runSession.subscribe((event) => {
|
|
255
|
+
liveAdapter.handle(event);
|
|
256
|
+
observePromptEvent(observation, event);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
try {
|
|
260
|
+
const modelSetup = buildModelSetupState(runSession.modelRegistry, runSession.model);
|
|
261
|
+
if (!prompt.startsWith("/") && modelSetup.requirement !== "ready") {
|
|
262
|
+
runSessionManager.appendMessage({ role: "user", content: prompt, timestamp: Date.now() });
|
|
263
|
+
const message =
|
|
264
|
+
modelSetup.requirement === "select_model"
|
|
265
|
+
? "Choose an available model before chat can run. OpenCandle found configured credentials but no active model."
|
|
266
|
+
: "Connect an AI model before chat can run. Paste a Google Gemini, OpenAI, or Anthropic API key in the setup panel.";
|
|
267
|
+
runSessionManager.appendCustomMessageEntry("opencandle-model-setup", message, true, {
|
|
268
|
+
source: "gui",
|
|
269
|
+
requirement: modelSetup.requirement,
|
|
270
|
+
});
|
|
271
|
+
options.wsHub.broadcastState();
|
|
272
|
+
} else {
|
|
273
|
+
await promptAndSettle(runSession, prompt, beforeIds, observation);
|
|
274
|
+
options.wsHub.broadcastState();
|
|
275
|
+
}
|
|
276
|
+
seq = liveAdapter.nextSeq();
|
|
277
|
+
if (seq === liveStartSeq) {
|
|
278
|
+
await waitForNewEntryId(
|
|
279
|
+
() => runSessionManager.getEntries().map((entry) => entry.id),
|
|
280
|
+
beforeIds,
|
|
281
|
+
);
|
|
282
|
+
const newEntries = runSessionManager
|
|
283
|
+
.getEntries()
|
|
284
|
+
.slice(beforeCount)
|
|
285
|
+
.filter((entry) => !beforeIds.has(entry.id));
|
|
286
|
+
const events = sessionEntriesToChatEvents(newEntries, {
|
|
287
|
+
sessionId,
|
|
288
|
+
updatedAt: new Date().toISOString(),
|
|
289
|
+
startSeq: seq,
|
|
290
|
+
});
|
|
291
|
+
for (const event of events) {
|
|
292
|
+
writeSse(res, event);
|
|
293
|
+
seq = event.seq + 1;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
writeSse(res, { type: "run.completed", runId, seq });
|
|
297
|
+
} catch (error) {
|
|
298
|
+
seq = liveAdapter.nextSeq();
|
|
299
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
300
|
+
writeSse(res, { type: "run.failed", runId, error: { message }, seq });
|
|
301
|
+
} finally {
|
|
302
|
+
unsubscribeLive();
|
|
303
|
+
res.end();
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
async function handleTrustedGuiMutation(
|
|
308
|
+
req: IncomingMessage,
|
|
309
|
+
res: ServerResponse,
|
|
310
|
+
options: GuiHttpRouteOptions,
|
|
311
|
+
action: (body: Record<string, unknown>) => Promise<void>,
|
|
312
|
+
): Promise<void> {
|
|
313
|
+
try {
|
|
314
|
+
await action(asRecord(await readJsonBody(req)));
|
|
315
|
+
writeJson(res, await options.wsHub.buildBootstrapPayload());
|
|
316
|
+
} catch (error) {
|
|
317
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
318
|
+
writeJson(res, { error: message }, message === "Read-only follower mode" ? 409 : 400);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function serveStaticAsset(
|
|
323
|
+
pathname: string,
|
|
324
|
+
res: ServerResponse,
|
|
325
|
+
options: GuiHttpRouteOptions,
|
|
326
|
+
): void {
|
|
327
|
+
const requested = pathname === "/" ? "/index.html" : pathname;
|
|
328
|
+
const path = resolve(join(options.webDist, requested));
|
|
329
|
+
if (!path.startsWith(options.webDist) || !existsSync(path)) {
|
|
330
|
+
const fallback = resolve(join(options.webDist, "index.html"));
|
|
331
|
+
if (!extname(requested) && fallback.startsWith(options.webDist) && existsSync(fallback)) {
|
|
332
|
+
res.writeHead(200, privateGuiHeaders("text/html; charset=utf-8", options));
|
|
333
|
+
createReadStream(fallback).pipe(res);
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
res.writeHead(404).end("Not found");
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const type = contentType(path);
|
|
341
|
+
res.writeHead(200, privateGuiHeaders(type, options));
|
|
342
|
+
createReadStream(path).pipe(res);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function writeJson(res: ServerResponse, value: unknown, status = 200): void {
|
|
346
|
+
res.writeHead(status, { "content-type": "application/json" });
|
|
347
|
+
res.end(JSON.stringify(value));
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function allowTrustedGuiRequest(
|
|
351
|
+
req: IncomingMessage,
|
|
352
|
+
res: ServerResponse,
|
|
353
|
+
label: string,
|
|
354
|
+
options: GuiHttpRouteOptions,
|
|
355
|
+
): boolean {
|
|
356
|
+
if (
|
|
357
|
+
isTrustedPrivateApiRequest(
|
|
358
|
+
req.headers,
|
|
359
|
+
options.privateApiSessionToken,
|
|
360
|
+
req.socket.remoteAddress,
|
|
361
|
+
{
|
|
362
|
+
allowRemote: options.allowRemotePrivateApi,
|
|
363
|
+
},
|
|
364
|
+
)
|
|
365
|
+
)
|
|
366
|
+
return true;
|
|
367
|
+
res.writeHead(403, { "content-type": "application/json" });
|
|
368
|
+
res.end(
|
|
369
|
+
JSON.stringify({
|
|
370
|
+
error: `${label} is only available to trusted GUI browser sessions.`,
|
|
371
|
+
}),
|
|
372
|
+
);
|
|
373
|
+
return false;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function privateGuiHeaders(
|
|
377
|
+
contentTypeValue: string,
|
|
378
|
+
options: GuiHttpRouteOptions,
|
|
379
|
+
): Record<string, string> {
|
|
380
|
+
const headers: Record<string, string> = { "content-type": contentTypeValue };
|
|
381
|
+
if (contentTypeValue.startsWith("text/html")) {
|
|
382
|
+
headers["set-cookie"] = privateApiCookieHeader(options.privateApiSessionToken);
|
|
383
|
+
}
|
|
384
|
+
return headers;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function writeSse(res: ServerResponse, event: ChatEvent): void {
|
|
388
|
+
res.write(`data: ${JSON.stringify(event)}\n\n`);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
async function readJsonBody(req: IncomingMessage): Promise<unknown> {
|
|
392
|
+
const chunks: Buffer[] = [];
|
|
393
|
+
for await (const chunk of req) {
|
|
394
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
395
|
+
}
|
|
396
|
+
if (chunks.length === 0) return {};
|
|
397
|
+
return JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function contentType(path: string): string {
|
|
401
|
+
switch (extname(path)) {
|
|
402
|
+
case ".html":
|
|
403
|
+
return "text/html; charset=utf-8";
|
|
404
|
+
case ".css":
|
|
405
|
+
return "text/css; charset=utf-8";
|
|
406
|
+
case ".js":
|
|
407
|
+
return "text/javascript; charset=utf-8";
|
|
408
|
+
case ".svg":
|
|
409
|
+
return "image/svg+xml";
|
|
410
|
+
default:
|
|
411
|
+
return "application/octet-stream";
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function asRecord(value: unknown): Record<string, unknown> {
|
|
416
|
+
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
417
|
+
? (value as Record<string, unknown>)
|
|
418
|
+
: {};
|
|
419
|
+
}
|
|
@@ -6,6 +6,7 @@ import { Value } from "@sinclair/typebox/value";
|
|
|
6
6
|
import { getDefaults } from "../../src/memory/tool-defaults.js";
|
|
7
7
|
import { wrapWithDefaults } from "../../src/runtime/tool-defaults-wrapper.js";
|
|
8
8
|
import { getAllTools } from "../../src/tools/index.js";
|
|
9
|
+
import type { AskUserHandler } from "../../src/types/index.js";
|
|
9
10
|
import { buildToolInvokeAckMessage } from "./tool-invoke-ack.js";
|
|
10
11
|
|
|
11
12
|
export interface InvokeToolResult {
|
|
@@ -30,6 +31,7 @@ export interface ToolInvokeControllerOptions {
|
|
|
30
31
|
onMarketStateChanged?: () => void;
|
|
31
32
|
getTools?: typeof getAllTools;
|
|
32
33
|
invokeTool?: typeof invokeToolFromUi;
|
|
34
|
+
askUserHandler?: AskUserHandler;
|
|
33
35
|
}
|
|
34
36
|
|
|
35
37
|
export function createToolInvokeController({
|
|
@@ -39,6 +41,7 @@ export function createToolInvokeController({
|
|
|
39
41
|
onMarketStateChanged,
|
|
40
42
|
getTools = getAllTools,
|
|
41
43
|
invokeTool = invokeToolFromUi,
|
|
44
|
+
askUserHandler,
|
|
42
45
|
}: ToolInvokeControllerOptions): ToolInvokeController {
|
|
43
46
|
async function handleToolInvoke(
|
|
44
47
|
toolName: string,
|
|
@@ -47,7 +50,7 @@ export function createToolInvokeController({
|
|
|
47
50
|
if (role !== "writer") throw new Error("Read-only follower mode");
|
|
48
51
|
const tool = getTools().find((candidate) => candidate.name === toolName);
|
|
49
52
|
if (!tool) throw new Error(`Unknown tool: ${toolName}`);
|
|
50
|
-
const result = await invokeTool(getSessionManager(), tool, args, "ui");
|
|
53
|
+
const result = await invokeTool(getSessionManager(), tool, args, "ui", { askUserHandler });
|
|
51
54
|
if (!result.isError && marketStateToolMapping(toolName) != null) {
|
|
52
55
|
onMarketStateChanged?.();
|
|
53
56
|
}
|
|
@@ -94,7 +97,7 @@ export async function invokeToolFromUi(
|
|
|
94
97
|
tool: AgentTool<TSchema, unknown>,
|
|
95
98
|
args: Record<string, unknown>,
|
|
96
99
|
source: "ui" | "background" = "ui",
|
|
97
|
-
options: { recordTranscript?: boolean } = {},
|
|
100
|
+
options: { askUserHandler?: AskUserHandler; recordTranscript?: boolean } = {},
|
|
98
101
|
): Promise<InvokeToolResult> {
|
|
99
102
|
if (!Value.Check(tool.parameters, args)) {
|
|
100
103
|
const errors = [...Value.Errors(tool.parameters, args)]
|
|
@@ -130,7 +133,18 @@ export async function invokeToolFromUi(
|
|
|
130
133
|
let result: AgentToolResult<unknown>;
|
|
131
134
|
let isError = false;
|
|
132
135
|
try {
|
|
133
|
-
result = await
|
|
136
|
+
result = await (
|
|
137
|
+
wrapped.execute as (
|
|
138
|
+
id: string,
|
|
139
|
+
params: never,
|
|
140
|
+
signal: AbortSignal | undefined,
|
|
141
|
+
onUpdate: undefined,
|
|
142
|
+
ctx: { askUserHandler?: AskUserHandler; hasUI: false },
|
|
143
|
+
) => ReturnType<typeof wrapped.execute>
|
|
144
|
+
)(toolCallId, args as never, undefined, undefined, {
|
|
145
|
+
askUserHandler: options.askUserHandler,
|
|
146
|
+
hasUI: false,
|
|
147
|
+
});
|
|
134
148
|
} catch (error) {
|
|
135
149
|
isError = true;
|
|
136
150
|
result = {
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import type { Api, Model } from "@earendil-works/pi-ai";
|
|
2
2
|
import { persistProviderCredential } from "../../src/onboarding/connect.js";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
getCredentialSource,
|
|
5
|
+
isApiKeyProvider,
|
|
6
|
+
PROVIDERS,
|
|
7
|
+
} from "../../src/onboarding/providers.js";
|
|
4
8
|
import { validateCredential } from "../../src/onboarding/validation.js";
|
|
5
9
|
|
|
6
10
|
export type ModelSetupRequirement = "ready" | "select_model" | "connect_auth";
|
|
@@ -193,6 +197,9 @@ export function createModelSetupController({
|
|
|
193
197
|
|
|
194
198
|
const descriptor = PROVIDERS.find((candidate) => candidate.id === providerId);
|
|
195
199
|
if (!descriptor) throw new Error(`Unknown provider: ${providerId}`);
|
|
200
|
+
if (!isApiKeyProvider(descriptor)) {
|
|
201
|
+
throw new Error(`${descriptor.displayName} is not configured with an API key.`);
|
|
202
|
+
}
|
|
196
203
|
|
|
197
204
|
if (getCredentialSource(descriptor.id) === "env") {
|
|
198
205
|
throw new Error(
|
|
@@ -203,7 +210,7 @@ export function createModelSetupController({
|
|
|
203
210
|
const trimmed = apiKey.trim();
|
|
204
211
|
if (!trimmed) throw new Error(`Paste a ${descriptor.displayName} API key first.`);
|
|
205
212
|
|
|
206
|
-
const validation = await validateCredential(descriptor.id
|
|
213
|
+
const validation = await validateCredential(descriptor.id, trimmed);
|
|
207
214
|
if (validation.status === "invalid") {
|
|
208
215
|
const statusHint =
|
|
209
216
|
validation.httpStatus !== undefined ? ` (HTTP ${validation.httpStatus})` : "";
|
|
@@ -213,7 +220,7 @@ export function createModelSetupController({
|
|
|
213
220
|
);
|
|
214
221
|
}
|
|
215
222
|
|
|
216
|
-
persistProviderCredential(descriptor.id
|
|
223
|
+
persistProviderCredential(descriptor.id, trimmed);
|
|
217
224
|
|
|
218
225
|
const verifiedNote =
|
|
219
226
|
validation.status === "transient"
|
package/gui/server/projector.ts
CHANGED
|
@@ -36,6 +36,7 @@ const DIRECT_TOOL_GAP_PROVIDERS: Record<string, string> = {
|
|
|
36
36
|
compare_companies: "alpha_vantage",
|
|
37
37
|
get_economic_data: "fred",
|
|
38
38
|
get_twitter_sentiment: "twitter",
|
|
39
|
+
get_reddit_sentiment: "reddit",
|
|
39
40
|
};
|
|
40
41
|
|
|
41
42
|
export function createEmptyDashboardState(): DashboardState {
|
|
@@ -213,7 +214,8 @@ function parseSkippedProviders(text: string): string[] {
|
|
|
213
214
|
|
|
214
215
|
function parseSoftGapProviders(text: string): string[] {
|
|
215
216
|
const providers: string[] = [];
|
|
216
|
-
const re =
|
|
217
|
+
const re =
|
|
218
|
+
/\[OPENCANDLE_(?:SKIPPED|SOFT_DEGRADED|EXTERNAL_TOOL_REQUIRED)[^\]]*provider=([a-z0-9_-]+)/gi;
|
|
217
219
|
while (true) {
|
|
218
220
|
const match = re.exec(text);
|
|
219
221
|
if (!match) break;
|
|
@@ -226,7 +228,9 @@ function inferDirectToolGapProvider(
|
|
|
226
228
|
toolName: string | undefined,
|
|
227
229
|
text: string,
|
|
228
230
|
): string | undefined {
|
|
229
|
-
if (!toolName || !/(?:⚠|unavailable|No .*data found|
|
|
231
|
+
if (!toolName || !/(?:⚠|unavailable|No .*data found|EXTERNAL_TOOL_SETUP)/i.test(text)) {
|
|
232
|
+
return undefined;
|
|
233
|
+
}
|
|
230
234
|
return DIRECT_TOOL_GAP_PROVIDERS[toolName];
|
|
231
235
|
}
|
|
232
236
|
|