opencandle 0.10.0 → 0.11.1
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 +12 -9
- package/dist/analysts/orchestrator.d.ts +3 -4
- package/dist/analysts/orchestrator.js +34 -44
- package/dist/analysts/orchestrator.js.map +1 -1
- package/dist/cli-main.js +242 -9
- package/dist/cli-main.js.map +1 -1
- package/dist/config.d.ts +4 -9
- package/dist/config.js +7 -10
- package/dist/config.js.map +1 -1
- package/dist/doctor/report.js +1 -1
- package/dist/doctor/report.js.map +1 -1
- package/dist/infra/cache.d.ts +6 -0
- package/dist/infra/cache.js +16 -6
- package/dist/infra/cache.js.map +1 -1
- package/dist/infra/freshness.d.ts +21 -0
- package/dist/infra/freshness.js +119 -0
- package/dist/infra/freshness.js.map +1 -0
- package/dist/infra/market-calendar.d.ts +14 -0
- package/dist/infra/market-calendar.js +137 -0
- package/dist/infra/market-calendar.js.map +1 -0
- package/dist/infra/rate-limiter.js +1 -0
- package/dist/infra/rate-limiter.js.map +1 -1
- package/dist/market-state/daily-report.d.ts +1 -1
- package/dist/market-state/daily-report.js +5 -16
- package/dist/market-state/daily-report.js.map +1 -1
- package/dist/market-state/service.d.ts +0 -35
- package/dist/market-state/service.js +0 -63
- package/dist/market-state/service.js.map +1 -1
- package/dist/market-state/summaries.d.ts +8 -0
- package/dist/market-state/summaries.js +70 -0
- package/dist/market-state/summaries.js.map +1 -0
- package/dist/memory/sqlite.js +20 -19
- package/dist/memory/sqlite.js.map +1 -1
- package/dist/onboarding/providers.d.ts +25 -1
- package/dist/onboarding/providers.js +26 -0
- package/dist/onboarding/providers.js.map +1 -1
- package/dist/pi/opencandle-extension.js +57 -226
- package/dist/pi/opencandle-extension.js.map +1 -1
- package/dist/pi/session-action-dedupe.d.ts +6 -0
- package/dist/pi/session-action-dedupe.js +126 -0
- package/dist/pi/session-action-dedupe.js.map +1 -0
- package/dist/pi/session-writer-lock.d.ts +26 -2
- package/dist/pi/session-writer-lock.js +230 -18
- package/dist/pi/session-writer-lock.js.map +1 -1
- package/dist/pi/setup.js +5 -5
- package/dist/pi/setup.js.map +1 -1
- package/dist/pi/tui-session-coordinator.d.ts +15 -0
- package/dist/pi/tui-session-coordinator.js +283 -0
- package/dist/pi/tui-session-coordinator.js.map +1 -0
- package/dist/prompts/context-builder.js +1 -1
- package/dist/prompts/policy-cards.js +1 -1
- package/dist/prompts/policy-cards.js.map +1 -1
- package/dist/providers/alpha-vantage.js +6 -0
- package/dist/providers/alpha-vantage.js.map +1 -1
- package/dist/providers/coingecko.js +1 -0
- package/dist/providers/coingecko.js.map +1 -1
- package/dist/providers/polymarket.d.ts +2 -0
- package/dist/providers/polymarket.js +163 -0
- package/dist/providers/polymarket.js.map +1 -0
- package/dist/providers/wrap-provider.js +5 -3
- package/dist/providers/wrap-provider.js.map +1 -1
- package/dist/providers/yahoo-finance.d.ts +3 -0
- package/dist/providers/yahoo-finance.js +206 -10
- package/dist/providers/yahoo-finance.js.map +1 -1
- package/dist/routing/classify-intent.js +4 -5
- package/dist/routing/classify-intent.js.map +1 -1
- package/dist/routing/entity-extractor.js +16 -0
- package/dist/routing/entity-extractor.js.map +1 -1
- package/dist/routing/route-manifest.js +1 -2
- package/dist/routing/route-manifest.js.map +1 -1
- package/dist/routing/router-llm-client.js +18 -3
- package/dist/routing/router-llm-client.js.map +1 -1
- package/dist/routing/router-prompt.js +1 -1
- package/dist/routing/router.d.ts +1 -1
- package/dist/routing/router.js +309 -9
- package/dist/routing/router.js.map +1 -1
- package/dist/routing/slot-resolver.d.ts +1 -0
- package/dist/routing/slot-resolver.js +1 -1
- package/dist/routing/slot-resolver.js.map +1 -1
- package/dist/runtime/evidence.d.ts +1 -0
- package/dist/runtime/evidence.js.map +1 -1
- package/dist/runtime/numeric-claims.d.ts +23 -0
- package/dist/runtime/numeric-claims.js +100 -0
- package/dist/runtime/numeric-claims.js.map +1 -0
- package/dist/runtime/planning-evidence.js +1 -76
- package/dist/runtime/planning-evidence.js.map +1 -1
- package/dist/runtime/prompt-step.d.ts +10 -1
- package/dist/runtime/prompt-step.js +116 -2
- package/dist/runtime/prompt-step.js.map +1 -1
- package/dist/runtime/session-coordinator.d.ts +11 -1
- package/dist/runtime/session-coordinator.js +354 -62
- package/dist/runtime/session-coordinator.js.map +1 -1
- package/dist/runtime/validation.d.ts +3 -1
- package/dist/runtime/workflow-events.d.ts +1 -1
- package/dist/runtime/workflow-events.js.map +1 -1
- package/dist/runtime/workflow-types.d.ts +3 -0
- package/dist/runtime/workflow-types.js.map +1 -1
- package/dist/system-prompt.js +1 -1
- package/dist/tools/fundamentals/comps.d.ts +1 -0
- package/dist/tools/fundamentals/comps.js +20 -9
- package/dist/tools/fundamentals/comps.js.map +1 -1
- package/dist/tools/fundamentals/dcf.d.ts +1 -1
- package/dist/tools/fundamentals/dcf.js +190 -76
- package/dist/tools/fundamentals/dcf.js.map +1 -1
- package/dist/tools/index.d.ts +1 -1
- package/dist/tools/index.js +3 -3
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/macro/event-probabilities.d.ts +8 -0
- package/dist/tools/macro/event-probabilities.js +121 -0
- package/dist/tools/macro/event-probabilities.js.map +1 -0
- package/dist/tools/market/crypto-price.d.ts +4 -1
- package/dist/tools/market/crypto-price.js +10 -1
- package/dist/tools/market/crypto-price.js.map +1 -1
- package/dist/tools/market/screen-stocks.js +14 -3
- package/dist/tools/market/screen-stocks.js.map +1 -1
- package/dist/tools/market/stock-quote.d.ts +4 -1
- package/dist/tools/market/stock-quote.js +9 -4
- package/dist/tools/market/stock-quote.js.map +1 -1
- package/dist/tools/options/option-chain.d.ts +4 -1
- package/dist/tools/options/option-chain.js +12 -1
- package/dist/tools/options/option-chain.js.map +1 -1
- package/dist/tools/portfolio/daily-report.js +10 -4
- package/dist/tools/portfolio/daily-report.js.map +1 -1
- package/dist/tools/portfolio/risk-analysis.js +1 -1
- package/dist/tools/portfolio/risk-analysis.js.map +1 -1
- package/dist/tools/technical/backtest.d.ts +23 -5
- package/dist/tools/technical/backtest.js +131 -94
- package/dist/tools/technical/backtest.js.map +1 -1
- package/dist/tools/technical/indicators.js +1 -1
- package/dist/tools/technical/indicators.js.map +1 -1
- package/dist/types/fundamentals.d.ts +1 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/market.d.ts +2 -0
- package/dist/types/options.d.ts +1 -0
- package/dist/types/prediction-markets.d.ts +13 -0
- package/dist/types/prediction-markets.js +2 -0
- package/dist/types/prediction-markets.js.map +1 -0
- package/gui/server/chat-event-adapter.ts +89 -5
- package/gui/server/http-routes.ts +661 -37
- package/gui/server/invoke-tool.ts +165 -16
- package/gui/server/live-chat-event-adapter.ts +37 -2
- package/gui/server/local-session-coordinator.ts +97 -0
- package/gui/server/market-state-api.ts +64 -48
- package/gui/server/projector.ts +132 -3
- package/gui/server/server.ts +51 -6
- package/gui/server/session-actions.ts +146 -45
- package/gui/server/session-entry-wait.ts +22 -3
- package/gui/server/tool-metadata.ts +3 -1
- package/gui/server/ws-hub.ts +77 -10
- package/gui/shared/chat-events.ts +10 -1
- package/gui/shared/event-reducer.ts +1 -0
- package/gui/web/dist/assets/CatalogOverlay-DZ1niyQm.js +1 -0
- package/gui/web/dist/assets/index-D4F9AJnn.css +2 -0
- package/gui/web/dist/assets/index-DtqMMBTr.js +65 -0
- package/gui/web/dist/index.html +2 -2
- package/package.json +13 -15
- package/src/analysts/orchestrator.ts +35 -56
- package/src/cli-main.ts +253 -13
- package/src/config.ts +12 -20
- package/src/doctor/report.ts +1 -1
- package/src/infra/cache.ts +24 -11
- package/src/infra/freshness.ts +165 -0
- package/src/infra/market-calendar.ts +193 -0
- package/src/infra/rate-limiter.ts +1 -0
- package/src/market-state/daily-report.ts +6 -16
- package/src/market-state/service.ts +0 -136
- package/src/market-state/summaries.ts +75 -0
- package/src/memory/sqlite.ts +23 -19
- package/src/onboarding/providers.ts +29 -1
- package/src/pi/opencandle-extension.ts +67 -270
- package/src/pi/session-action-dedupe.ts +155 -0
- package/src/pi/session-writer-lock.ts +290 -20
- package/src/pi/setup.ts +6 -6
- package/src/pi/tui-session-coordinator.ts +351 -0
- package/src/prompts/context-builder.ts +1 -1
- package/src/prompts/policy-cards.ts +1 -1
- package/src/providers/alpha-vantage.ts +6 -0
- package/src/providers/coingecko.ts +2 -0
- package/src/providers/polymarket.ts +214 -0
- package/src/providers/wrap-provider.ts +5 -3
- package/src/providers/yahoo-finance.ts +339 -27
- package/src/routing/classify-intent.ts +4 -5
- package/src/routing/entity-extractor.ts +17 -0
- package/src/routing/route-manifest.ts +1 -2
- package/src/routing/router-llm-client.ts +31 -19
- package/src/routing/router-prompt.ts +1 -1
- package/src/routing/router.ts +379 -8
- package/src/routing/slot-resolver.ts +1 -1
- package/src/runtime/evidence.ts +1 -0
- package/src/runtime/numeric-claims.ts +108 -0
- package/src/runtime/planning-evidence.ts +6 -91
- package/src/runtime/prompt-step.ts +127 -2
- package/src/runtime/session-coordinator.ts +464 -74
- package/src/runtime/validation.ts +1 -1
- package/src/runtime/workflow-events.ts +2 -0
- package/src/runtime/workflow-types.ts +3 -0
- package/src/system-prompt.ts +1 -1
- package/src/tools/AGENTS.md +1 -1
- package/src/tools/fundamentals/comps.ts +25 -14
- package/src/tools/fundamentals/dcf.ts +217 -87
- package/src/tools/index.ts +3 -3
- package/src/tools/macro/event-probabilities.ts +141 -0
- package/src/tools/market/crypto-price.ts +14 -2
- package/src/tools/market/screen-stocks.ts +14 -3
- package/src/tools/market/stock-quote.ts +49 -43
- package/src/tools/options/option-chain.ts +16 -2
- package/src/tools/portfolio/daily-report.ts +10 -4
- package/src/tools/portfolio/risk-analysis.ts +1 -1
- package/src/tools/technical/backtest.ts +167 -108
- package/src/tools/technical/indicators.ts +1 -1
- package/src/types/fundamentals.ts +1 -0
- package/src/types/index.ts +1 -0
- package/src/types/market.ts +2 -0
- package/src/types/options.ts +1 -0
- package/src/types/prediction-markets.ts +13 -0
- package/dist/tools/portfolio/predictions.d.ts +0 -55
- package/dist/tools/portfolio/predictions.js +0 -422
- package/dist/tools/portfolio/predictions.js.map +0 -1
- package/gui/server/chat-run-session.ts +0 -16
- package/gui/web/dist/assets/CatalogOverlay-CYptsda-.js +0 -1
- package/gui/web/dist/assets/index-B7QAjY5g.js +0 -65
- package/gui/web/dist/assets/index-D5dbWPfM.css +0 -2
- package/src/tools/portfolio/predictions.ts +0 -553
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { createServer, type IncomingMessage, type ServerResponse } from "node:http";
|
|
3
|
+
import type { AddressInfo } from "node:net";
|
|
4
|
+
import type { AgentSession, SessionManager } from "@earendil-works/pi-coding-agent";
|
|
5
|
+
import {
|
|
6
|
+
clearPendingSessionAction,
|
|
7
|
+
hasAcceptedSessionAction,
|
|
8
|
+
hasPendingSessionAction,
|
|
9
|
+
recordAcceptedSessionAction,
|
|
10
|
+
recordPendingSessionAction,
|
|
11
|
+
} from "./session-action-dedupe.js";
|
|
12
|
+
|
|
13
|
+
interface TuiSessionCoordinatorOptions {
|
|
14
|
+
getSession: () => AgentSession;
|
|
15
|
+
getSessionManager: () => SessionManager;
|
|
16
|
+
getModelUnavailableMessage: () => string | null;
|
|
17
|
+
syncWriterLockScope?: () => void;
|
|
18
|
+
now?: () => number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface TuiSessionCoordinatorServer {
|
|
22
|
+
endpoint: string;
|
|
23
|
+
secret: string;
|
|
24
|
+
close(): Promise<void>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface AcceptedAction {
|
|
28
|
+
expiresAt: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const DEDUPE_RETENTION_MS = 10 * 60 * 1000;
|
|
32
|
+
|
|
33
|
+
export async function startTuiSessionCoordinatorServer(
|
|
34
|
+
options: TuiSessionCoordinatorOptions,
|
|
35
|
+
): Promise<TuiSessionCoordinatorServer> {
|
|
36
|
+
const now = options.now ?? Date.now;
|
|
37
|
+
const secret = randomBytes(32).toString("base64url");
|
|
38
|
+
const acceptedActions = new Map<string, AcceptedAction>();
|
|
39
|
+
const activeRunSessions = new Set<string>();
|
|
40
|
+
|
|
41
|
+
const server = createServer((req, res) => {
|
|
42
|
+
void handleRequest(req, res).catch((error) => {
|
|
43
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
44
|
+
writeJson(res, { error: message }, 500);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
await new Promise<void>((resolve, reject) => {
|
|
49
|
+
server.once("error", reject);
|
|
50
|
+
server.listen(0, "127.0.0.1", () => {
|
|
51
|
+
server.off("error", reject);
|
|
52
|
+
resolve();
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const address = server.address() as AddressInfo;
|
|
57
|
+
const endpoint = `http://127.0.0.1:${address.port}`;
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
endpoint,
|
|
61
|
+
secret,
|
|
62
|
+
close: () =>
|
|
63
|
+
new Promise<void>((resolve, reject) => {
|
|
64
|
+
server.close((error) => (error ? reject(error) : resolve()));
|
|
65
|
+
}),
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
async function handleRequest(req: IncomingMessage, res: ServerResponse): Promise<void> {
|
|
69
|
+
const url = new URL(req.url ?? "/", endpoint);
|
|
70
|
+
if (url.pathname !== "/api/local-coordinator/chat-run" || req.method !== "POST") {
|
|
71
|
+
writeJson(res, { error: "Not found" }, 404);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (
|
|
76
|
+
!isLoopbackAddress(req.socket.remoteAddress) ||
|
|
77
|
+
req.headers["x-opencandle-coordinator-secret"] !== secret
|
|
78
|
+
) {
|
|
79
|
+
writeJson(res, { error: "Local coordinator request was not authorized." }, 403);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const body = asRecord(await readJsonBody(req));
|
|
84
|
+
const prompt = String(body.prompt ?? "").trim();
|
|
85
|
+
const requestedSessionId = String(body.sessionId ?? "").trim();
|
|
86
|
+
const actionId = String(body.actionId ?? "").trim() || `legacy-tui-chat-${now()}`;
|
|
87
|
+
const sessionManager = options.getSessionManager();
|
|
88
|
+
const sessionId = sessionManager.getSessionId();
|
|
89
|
+
if (!prompt) {
|
|
90
|
+
writeJson(res, { error: "prompt is required" }, 400);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
if (requestedSessionId && requestedSessionId !== sessionId) {
|
|
94
|
+
writeJson(
|
|
95
|
+
res,
|
|
96
|
+
{ error: "Session route and request body disagree", code: "session_changed" },
|
|
97
|
+
409,
|
|
98
|
+
);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
try {
|
|
102
|
+
options.syncWriterLockScope?.();
|
|
103
|
+
} catch (error) {
|
|
104
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
105
|
+
writeJson(res, { error: message, code: "syncing" }, 409);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
pruneExpiredActions();
|
|
110
|
+
const actionKey = `${sessionId}:${actionId}`;
|
|
111
|
+
if (acceptedActions.has(actionKey) || hasAcceptedSessionAction(sessionManager, actionId)) {
|
|
112
|
+
writeJson(res, { ok: true, duplicate: true });
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
if (hasPendingSessionAction(sessionManager, actionId)) {
|
|
116
|
+
writeJson(
|
|
117
|
+
res,
|
|
118
|
+
{ error: "OpenCandle is reconnecting to this session.", code: "syncing" },
|
|
119
|
+
409,
|
|
120
|
+
);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
let session: AgentSession;
|
|
124
|
+
try {
|
|
125
|
+
session = options.getSession();
|
|
126
|
+
} catch (error) {
|
|
127
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
128
|
+
writeJson(res, { error: message, code: "session_starting" }, 409);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
if (
|
|
132
|
+
activeRunSessions.has(sessionId) ||
|
|
133
|
+
session.isStreaming ||
|
|
134
|
+
session.pendingMessageCount > 0
|
|
135
|
+
) {
|
|
136
|
+
writeJson(
|
|
137
|
+
res,
|
|
138
|
+
{
|
|
139
|
+
error: "OpenCandle is still working in this session. Try again when it finishes.",
|
|
140
|
+
code: "session_busy",
|
|
141
|
+
},
|
|
142
|
+
409,
|
|
143
|
+
);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
activeRunSessions.add(sessionId);
|
|
148
|
+
try {
|
|
149
|
+
recordPendingSessionAction(sessionManager, actionId);
|
|
150
|
+
let actionAccepted = false;
|
|
151
|
+
const recordAcceptedAction = () => {
|
|
152
|
+
if (actionAccepted) return;
|
|
153
|
+
recordAcceptedSessionAction(sessionManager, actionId);
|
|
154
|
+
options.syncWriterLockScope?.();
|
|
155
|
+
acceptedActions.set(actionKey, { expiresAt: now() + DEDUPE_RETENTION_MS });
|
|
156
|
+
actionAccepted = true;
|
|
157
|
+
};
|
|
158
|
+
let completed: boolean;
|
|
159
|
+
try {
|
|
160
|
+
completed = await streamPromptRun(res, prompt, sessionId, recordAcceptedAction);
|
|
161
|
+
} catch (error) {
|
|
162
|
+
if (!actionAccepted) clearPendingSessionAction(sessionManager, actionId);
|
|
163
|
+
throw error;
|
|
164
|
+
}
|
|
165
|
+
if (completed) {
|
|
166
|
+
recordAcceptedAction();
|
|
167
|
+
} else if (!actionAccepted) {
|
|
168
|
+
clearPendingSessionAction(sessionManager, actionId);
|
|
169
|
+
}
|
|
170
|
+
} finally {
|
|
171
|
+
activeRunSessions.delete(sessionId);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async function streamPromptRun(
|
|
176
|
+
res: ServerResponse,
|
|
177
|
+
prompt: string,
|
|
178
|
+
sessionId: string,
|
|
179
|
+
onPromptAdmitted: () => void,
|
|
180
|
+
): Promise<boolean> {
|
|
181
|
+
options.syncWriterLockScope?.();
|
|
182
|
+
const session = options.getSession();
|
|
183
|
+
const sessionManager = options.getSessionManager();
|
|
184
|
+
const beforeEntries = sessionManager.getEntries();
|
|
185
|
+
const beforeCount = beforeEntries.length;
|
|
186
|
+
let seq = 1;
|
|
187
|
+
const runId = `tui-run-${now()}`;
|
|
188
|
+
|
|
189
|
+
res.writeHead(200, {
|
|
190
|
+
"content-type": "text/event-stream; charset=utf-8",
|
|
191
|
+
"cache-control": "no-cache, no-transform",
|
|
192
|
+
connection: "keep-alive",
|
|
193
|
+
});
|
|
194
|
+
writeSse(res, { type: "run.started", runId, sessionId, seq: seq++ });
|
|
195
|
+
res.flushHeaders?.();
|
|
196
|
+
|
|
197
|
+
try {
|
|
198
|
+
const modelUnavailableMessage = options.getModelUnavailableMessage();
|
|
199
|
+
if (!prompt.startsWith("/") && modelUnavailableMessage) {
|
|
200
|
+
sessionManager.appendMessage({ role: "user", content: prompt, timestamp: now() });
|
|
201
|
+
onPromptAdmitted();
|
|
202
|
+
options.syncWriterLockScope?.();
|
|
203
|
+
sessionManager.appendCustomMessageEntry(
|
|
204
|
+
"opencandle-model-setup",
|
|
205
|
+
modelUnavailableMessage,
|
|
206
|
+
true,
|
|
207
|
+
{
|
|
208
|
+
source: "tui",
|
|
209
|
+
},
|
|
210
|
+
);
|
|
211
|
+
} else {
|
|
212
|
+
const unsubscribe = subscribeToPromptAdmission(session, prompt, onPromptAdmitted);
|
|
213
|
+
try {
|
|
214
|
+
await session.prompt(prompt);
|
|
215
|
+
options.syncWriterLockScope?.();
|
|
216
|
+
await waitForTurnSettlement(session);
|
|
217
|
+
} finally {
|
|
218
|
+
unsubscribe?.();
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
const newEntries = sessionManager.getEntries().slice(beforeCount);
|
|
222
|
+
for (const event of entriesToChatEvents(newEntries, sessionId, seq)) {
|
|
223
|
+
writeSse(res, event);
|
|
224
|
+
seq = event.seq + 1;
|
|
225
|
+
}
|
|
226
|
+
writeSse(res, { type: "run.completed", runId, sessionId, seq });
|
|
227
|
+
return true;
|
|
228
|
+
} catch (error) {
|
|
229
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
230
|
+
writeSse(res, { type: "run.failed", runId, sessionId, error: { message }, seq });
|
|
231
|
+
return false;
|
|
232
|
+
} finally {
|
|
233
|
+
res.end();
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function pruneExpiredActions(): void {
|
|
238
|
+
const currentTime = now();
|
|
239
|
+
for (const [actionKey, accepted] of acceptedActions) {
|
|
240
|
+
if (accepted.expiresAt <= currentTime) acceptedActions.delete(actionKey);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function subscribeToPromptAdmission(
|
|
245
|
+
session: AgentSession,
|
|
246
|
+
prompt: string,
|
|
247
|
+
onPromptAdmitted: () => void,
|
|
248
|
+
): (() => void) | undefined {
|
|
249
|
+
if (prompt.startsWith("/")) return undefined;
|
|
250
|
+
const maybeSession = session as AgentSession & {
|
|
251
|
+
subscribe?: (handler: (event: unknown) => void) => () => void;
|
|
252
|
+
};
|
|
253
|
+
if (typeof maybeSession.subscribe !== "function") return undefined;
|
|
254
|
+
return maybeSession.subscribe((event) => {
|
|
255
|
+
const record = asRecord(event);
|
|
256
|
+
if (record.type !== "message_start") return;
|
|
257
|
+
const message = asRecord(record.message);
|
|
258
|
+
if (message.role !== "user") return;
|
|
259
|
+
if (messageTextFromContent(message.content).trim() === prompt.trim()) onPromptAdmitted();
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
async function waitForTurnSettlement(session: AgentSession): Promise<void> {
|
|
265
|
+
const startedAt = Date.now();
|
|
266
|
+
while (session.isStreaming || session.pendingMessageCount > 0) {
|
|
267
|
+
if (Date.now() - startedAt > 120_000) {
|
|
268
|
+
throw new Error("Timed out waiting for session turn to settle");
|
|
269
|
+
}
|
|
270
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function entriesToChatEvents(
|
|
275
|
+
entries: unknown[],
|
|
276
|
+
sessionId: string,
|
|
277
|
+
startSeq: number,
|
|
278
|
+
): Array<Record<string, unknown> & { seq: number }> {
|
|
279
|
+
let seq = startSeq;
|
|
280
|
+
const events: Array<Record<string, unknown> & { seq: number }> = [];
|
|
281
|
+
for (const entry of entries) {
|
|
282
|
+
const record = asRecord(entry);
|
|
283
|
+
const message = asRecord(record.message);
|
|
284
|
+
const source = Object.keys(message).length > 0 ? message : record;
|
|
285
|
+
const role =
|
|
286
|
+
typeof source.role === "string"
|
|
287
|
+
? source.role
|
|
288
|
+
: record.type === "custom_message"
|
|
289
|
+
? "assistant"
|
|
290
|
+
: undefined;
|
|
291
|
+
const content = normalizeContent(source.content);
|
|
292
|
+
if (!role || content.length === 0) continue;
|
|
293
|
+
events.push({
|
|
294
|
+
type: "message.completed",
|
|
295
|
+
sessionId,
|
|
296
|
+
messageId: String(record.id ?? `tui-entry-${seq}`),
|
|
297
|
+
role,
|
|
298
|
+
content,
|
|
299
|
+
seq: seq++,
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
return events;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function normalizeContent(content: unknown): Array<Record<string, unknown>> {
|
|
306
|
+
if (typeof content === "string") return [{ type: "text", text: content }];
|
|
307
|
+
if (Array.isArray(content)) return content.map(asRecord).filter((item) => item.type);
|
|
308
|
+
const record = asRecord(content);
|
|
309
|
+
if (typeof record.text === "string") return [{ type: "text", text: record.text }];
|
|
310
|
+
return [];
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function messageTextFromContent(content: unknown): string {
|
|
314
|
+
if (typeof content === "string") return content;
|
|
315
|
+
if (!Array.isArray(content)) return "";
|
|
316
|
+
return content
|
|
317
|
+
.map((part) =>
|
|
318
|
+
typeof part === "object" && part !== null && "text" in part && typeof part.text === "string"
|
|
319
|
+
? part.text
|
|
320
|
+
: "",
|
|
321
|
+
)
|
|
322
|
+
.join("");
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function writeJson(res: ServerResponse, value: unknown, status = 200): void {
|
|
326
|
+
res.writeHead(status, { "content-type": "application/json" });
|
|
327
|
+
res.end(JSON.stringify(value));
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function writeSse(res: ServerResponse, event: unknown): void {
|
|
331
|
+
res.write(`data: ${JSON.stringify(event)}\n\n`);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
async function readJsonBody(req: IncomingMessage): Promise<unknown> {
|
|
335
|
+
const chunks: Buffer[] = [];
|
|
336
|
+
for await (const chunk of req) {
|
|
337
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
338
|
+
}
|
|
339
|
+
if (chunks.length === 0) return {};
|
|
340
|
+
return JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function isLoopbackAddress(address: string | undefined): boolean {
|
|
344
|
+
return address === "127.0.0.1" || address === "::1" || address === "::ffff:127.0.0.1";
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function asRecord(value: unknown): Record<string, unknown> {
|
|
348
|
+
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
349
|
+
? (value as Record<string, unknown>)
|
|
350
|
+
: {};
|
|
351
|
+
}
|
|
@@ -291,7 +291,7 @@ const TOOL_CATALOG = `## Available Tools
|
|
|
291
291
|
- **Sentiment**: get_reddit_sentiment, get_twitter_sentiment, get_web_sentiment, get_sentiment_trend, get_sentiment_summary — retail and news sentiment from Reddit, Twitter/X, and web sources with historical trends and cross-source divergence detection
|
|
292
292
|
- **Web Search**: search_web — breaking news, earnings context, company events, regulatory developments. Supported freshness values are hours, day, week, and month; use category general with freshness month for broad industry context; never pass unsupported values such as all, year, 3mo, quarter, or custom date ranges. When a dedicated tool can answer the question (quotes, fundamentals, earnings, macro, SEC filings, sentiment), use that tool instead — do not add search_web as a supplementary source for data available through dedicated tools
|
|
293
293
|
- **Options**: get_option_chain — full options chain with strikes, bids/asks, volume, OI, IV, and computed Greeks (delta, gamma, theta, vega, rho)
|
|
294
|
-
- **Portfolio**: track_portfolio, analyze_risk, manage_watchlist, analyze_correlation, analyze_holdings_overlap,
|
|
294
|
+
- **Portfolio**: track_portfolio, analyze_risk, manage_watchlist, analyze_correlation, analyze_holdings_overlap, manage_alerts, daily_watchlist_report, manage_notifications — position tracking, P&L, Sharpe ratio, VaR, watchlist tracking, durable local alerts, daily watchlist reports, notification history, correlation matrix, and ETF/fund holdings overlap
|
|
295
295
|
- **User Interaction**: ask_user — ask the user a clarification question when their request is ambiguous or missing key details`;
|
|
296
296
|
|
|
297
297
|
function buildToolCatalog(addonDescriptions?: string[]): string {
|
|
@@ -141,7 +141,7 @@ For strategy backtest prompts, use backtest_strategy evidence before judging whe
|
|
|
141
141
|
status: "implemented",
|
|
142
142
|
capabilityGapIds: [],
|
|
143
143
|
content: `## Stateful Tracking Update Policy
|
|
144
|
-
For watchlist, portfolio tracking, alert management, daily-report
|
|
144
|
+
For watchlist, portfolio tracking, alert management, and daily-report prompts, use the state tool that owns the change or lookup: manage_watchlist, track_portfolio, manage_alerts, or daily_watchlist_report. Do not confirm a saved change from prose alone. Confirm the persisted state update with the symbol, action, quantity, cost, target, stop, or check result that the tool accepted. If the user asks to create an alert and check it now in the same request, call manage_alerts with the matching create action and check_after_create=true. If required fields for a state mutation are missing, ask the smallest clarification question instead of inventing values. For check or list operations, summarize the saved state and say clearly when no records exist. Do not turn a state update into a buy/sell recommendation unless the user separately asks for market analysis.`,
|
|
145
145
|
},
|
|
146
146
|
retail_finance_tradeoff: {
|
|
147
147
|
id: "retail_finance_tradeoff",
|
|
@@ -234,6 +234,7 @@ export async function getGlobalQuote(symbol: string, apiKey: string): Promise<St
|
|
|
234
234
|
week52High: 0, // Not available from GLOBAL_QUOTE
|
|
235
235
|
week52Low: 0, // Not available from GLOBAL_QUOTE
|
|
236
236
|
timestamp: Date.now(),
|
|
237
|
+
asOf: dateOnlyValue(gq["07. latest trading day"]),
|
|
237
238
|
};
|
|
238
239
|
|
|
239
240
|
cache.set(cacheKey, result, TTL.QUOTE);
|
|
@@ -246,6 +247,11 @@ export async function getGlobalQuote(symbol: string, apiKey: string): Promise<St
|
|
|
246
247
|
}
|
|
247
248
|
}
|
|
248
249
|
|
|
250
|
+
function dateOnlyValue(value: string | undefined): string | undefined {
|
|
251
|
+
if (!value || !/^\d{4}-\d{2}-\d{2}$/.test(value)) return undefined;
|
|
252
|
+
return value;
|
|
253
|
+
}
|
|
254
|
+
|
|
249
255
|
export async function getDailyHistory(
|
|
250
256
|
symbol: string,
|
|
251
257
|
apiKey: string,
|
|
@@ -19,6 +19,7 @@ interface CoinGeckoDetailResponse {
|
|
|
19
19
|
low_24h: { usd: number };
|
|
20
20
|
ath: { usd: number };
|
|
21
21
|
ath_date: { usd: string };
|
|
22
|
+
last_updated?: string;
|
|
22
23
|
circulating_supply: number;
|
|
23
24
|
total_supply: number | null;
|
|
24
25
|
};
|
|
@@ -52,6 +53,7 @@ export async function getCryptoPrice(id: string): Promise<CryptoPrice> {
|
|
|
52
53
|
circulatingSupply: md.circulating_supply,
|
|
53
54
|
totalSupply: md.total_supply,
|
|
54
55
|
timestamp: Date.now(),
|
|
56
|
+
asOf: md.last_updated,
|
|
55
57
|
};
|
|
56
58
|
|
|
57
59
|
cache.set(cacheKey, result, TTL.QUOTE);
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { cache, STALE_LIMIT, TTL } from "../infra/cache.js";
|
|
2
|
+
import { httpGet } from "../infra/http-client.js";
|
|
3
|
+
import { rateLimiter } from "../infra/rate-limiter.js";
|
|
4
|
+
import type { PredictionMarketQuote } from "../types/prediction-markets.js";
|
|
5
|
+
|
|
6
|
+
const BASE_URL = "https://gamma-api.polymarket.com";
|
|
7
|
+
const POLYMARKET_URL = "https://polymarket.com";
|
|
8
|
+
|
|
9
|
+
interface GammaSearchResponse {
|
|
10
|
+
events?: GammaEvent[];
|
|
11
|
+
markets?: GammaMarket[];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface GammaEvent {
|
|
15
|
+
id?: string | number;
|
|
16
|
+
slug?: string;
|
|
17
|
+
title?: string;
|
|
18
|
+
description?: string;
|
|
19
|
+
resolutionSource?: string;
|
|
20
|
+
endDate?: string;
|
|
21
|
+
volume?: unknown;
|
|
22
|
+
liquidity?: unknown;
|
|
23
|
+
updatedAt?: string;
|
|
24
|
+
markets?: GammaMarket[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface GammaMarket {
|
|
28
|
+
id?: string | number;
|
|
29
|
+
question?: string;
|
|
30
|
+
slug?: string;
|
|
31
|
+
description?: string;
|
|
32
|
+
resolutionSource?: string;
|
|
33
|
+
outcomes?: unknown;
|
|
34
|
+
outcomePrices?: unknown;
|
|
35
|
+
volume?: unknown;
|
|
36
|
+
volumeNum?: unknown;
|
|
37
|
+
liquidity?: unknown;
|
|
38
|
+
liquidityNum?: unknown;
|
|
39
|
+
endDate?: string;
|
|
40
|
+
endDateIso?: string;
|
|
41
|
+
updatedAt?: string;
|
|
42
|
+
active?: unknown;
|
|
43
|
+
closed?: unknown;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
interface MarketWithEvent {
|
|
47
|
+
event?: GammaEvent;
|
|
48
|
+
market: GammaMarket;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function searchPredictionMarkets(
|
|
52
|
+
query: string,
|
|
53
|
+
limit: number = 8,
|
|
54
|
+
): Promise<PredictionMarketQuote[]> {
|
|
55
|
+
const normalizedQuery = query.trim();
|
|
56
|
+
const normalizedLimit = normalizeLimit(limit);
|
|
57
|
+
if (!normalizedQuery) return [];
|
|
58
|
+
|
|
59
|
+
const cacheKey = `polymarket:search:${normalizedQuery.toLowerCase()}:${normalizedLimit}`;
|
|
60
|
+
const cached = cache.get<PredictionMarketQuote[]>(cacheKey);
|
|
61
|
+
if (cached) return cached;
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
await rateLimiter.acquire("polymarket");
|
|
65
|
+
const url = `${BASE_URL}/public-search?q=${encodeURIComponent(normalizedQuery)}&limit=${normalizedLimit}`;
|
|
66
|
+
const response = await httpGet<GammaSearchResponse>(url, {
|
|
67
|
+
headers: { Accept: "application/json" },
|
|
68
|
+
maxRetries: 0,
|
|
69
|
+
});
|
|
70
|
+
const quotes = decodeSearchResponse(response).slice(0, normalizedLimit);
|
|
71
|
+
cache.set(cacheKey, quotes, TTL.PREDICTION_MARKETS);
|
|
72
|
+
return quotes;
|
|
73
|
+
} catch (error) {
|
|
74
|
+
const stale = cache.getStale<PredictionMarketQuote[]>(cacheKey, STALE_LIMIT.PREDICTION_MARKETS);
|
|
75
|
+
if (stale) return stale.value;
|
|
76
|
+
throw error;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function decodeSearchResponse(response: GammaSearchResponse): PredictionMarketQuote[] {
|
|
81
|
+
const markets: MarketWithEvent[] = [
|
|
82
|
+
...(response.events ?? []).flatMap((event) =>
|
|
83
|
+
(event.markets ?? []).map((market) => ({ event, market })),
|
|
84
|
+
),
|
|
85
|
+
...(response.markets ?? []).map((market) => ({ market })),
|
|
86
|
+
];
|
|
87
|
+
return markets.flatMap(({ event, market }) =>
|
|
88
|
+
isOpenMarket(market) ? marketToQuotes(market, event) : [],
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function marketToQuotes(market: GammaMarket, event?: GammaEvent): PredictionMarketQuote[] {
|
|
93
|
+
const marketId = market.id === undefined ? undefined : String(market.id);
|
|
94
|
+
const title = stringValue(market.question) ?? stringValue(event?.title);
|
|
95
|
+
if (!marketId || !title) return [];
|
|
96
|
+
|
|
97
|
+
const outcomes = parseStringArray(market.outcomes);
|
|
98
|
+
const prices = parseNumberArray(market.outcomePrices);
|
|
99
|
+
if (outcomes.length === 0 || prices.length === 0) return [];
|
|
100
|
+
|
|
101
|
+
const resolutionCriteria = firstNonEmpty(market.description, market.resolutionSource);
|
|
102
|
+
const volumeUsd =
|
|
103
|
+
numberValue(market.volumeNum) ?? numberValue(market.volume) ?? numberValue(event?.volume);
|
|
104
|
+
const liquidityUsd =
|
|
105
|
+
numberValue(market.liquidityNum) ??
|
|
106
|
+
numberValue(market.liquidity) ??
|
|
107
|
+
numberValue(event?.liquidity);
|
|
108
|
+
const closeDate =
|
|
109
|
+
stringValue(market.endDate) ?? normalizeDateOnly(market.endDateIso ?? event?.endDate);
|
|
110
|
+
const url = marketUrl(market, event);
|
|
111
|
+
const asOf = stringValue(market.updatedAt) ?? stringValue(event?.updatedAt);
|
|
112
|
+
|
|
113
|
+
return outcomes.flatMap((outcome, index) => {
|
|
114
|
+
const probability = prices[index];
|
|
115
|
+
if (probability === undefined || probability < 0 || probability > 1) return [];
|
|
116
|
+
return [
|
|
117
|
+
{
|
|
118
|
+
source: "polymarket" as const,
|
|
119
|
+
marketId,
|
|
120
|
+
title,
|
|
121
|
+
outcome,
|
|
122
|
+
probability,
|
|
123
|
+
...(volumeUsd !== undefined && { volumeUsd }),
|
|
124
|
+
...(liquidityUsd !== undefined && { liquidityUsd }),
|
|
125
|
+
...(closeDate && { closeDate }),
|
|
126
|
+
...(resolutionCriteria && { resolutionCriteria }),
|
|
127
|
+
url,
|
|
128
|
+
...(asOf && { asOf }),
|
|
129
|
+
},
|
|
130
|
+
];
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function isOpenMarket(market: GammaMarket): boolean {
|
|
135
|
+
if (booleanValue(market.closed) === true) return false;
|
|
136
|
+
if (booleanValue(market.active) === false) return false;
|
|
137
|
+
const endDate = stringValue(market.endDate) ?? stringValue(market.endDateIso);
|
|
138
|
+
if (!endDate) return true;
|
|
139
|
+
const parsed = new Date(endDate);
|
|
140
|
+
return Number.isNaN(parsed.getTime()) || parsed.getTime() > Date.now();
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function normalizeLimit(limit: number): number {
|
|
144
|
+
if (!Number.isFinite(limit)) return 8;
|
|
145
|
+
return Math.min(50, Math.max(1, Math.floor(limit)));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function parseStringArray(value: unknown): string[] {
|
|
149
|
+
const parsed = parseJsonArray(value);
|
|
150
|
+
return parsed.flatMap((item) => {
|
|
151
|
+
const text = stringValue(item);
|
|
152
|
+
return text ? [text] : [];
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function parseNumberArray(value: unknown): Array<number | undefined> {
|
|
157
|
+
const parsed = parseJsonArray(value);
|
|
158
|
+
return parsed.map((item) => numberValue(item));
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function parseJsonArray(value: unknown): unknown[] {
|
|
162
|
+
if (Array.isArray(value)) return value;
|
|
163
|
+
if (typeof value !== "string") return [];
|
|
164
|
+
try {
|
|
165
|
+
const parsed = JSON.parse(value);
|
|
166
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
167
|
+
} catch {
|
|
168
|
+
return [];
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function numberValue(value: unknown): number | undefined {
|
|
173
|
+
const number =
|
|
174
|
+
typeof value === "number" ? value : typeof value === "string" ? Number(value) : NaN;
|
|
175
|
+
return Number.isFinite(number) ? number : undefined;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function stringValue(value: unknown): string | undefined {
|
|
179
|
+
if (typeof value !== "string") return undefined;
|
|
180
|
+
const trimmed = value.trim();
|
|
181
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function booleanValue(value: unknown): boolean | undefined {
|
|
185
|
+
if (typeof value === "boolean") return value;
|
|
186
|
+
if (typeof value !== "string") return undefined;
|
|
187
|
+
const normalized = value.trim().toLowerCase();
|
|
188
|
+
if (normalized === "true") return true;
|
|
189
|
+
if (normalized === "false") return false;
|
|
190
|
+
return undefined;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function firstNonEmpty(...values: unknown[]): string | undefined {
|
|
194
|
+
for (const value of values) {
|
|
195
|
+
const text = stringValue(value);
|
|
196
|
+
if (text) return text;
|
|
197
|
+
}
|
|
198
|
+
return undefined;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function normalizeDateOnly(value: string | undefined): string | undefined {
|
|
202
|
+
if (!value) return undefined;
|
|
203
|
+
return /^\d{4}-\d{2}-\d{2}$/.test(value) ? `${value}T00:00:00Z` : value;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function marketUrl(market: GammaMarket, event?: GammaEvent): string {
|
|
207
|
+
const marketSlug = stringValue(market.slug);
|
|
208
|
+
const eventSlug = stringValue(event?.slug);
|
|
209
|
+
if (eventSlug && marketSlug) {
|
|
210
|
+
return `${POLYMARKET_URL}/event/${encodeURIComponent(eventSlug)}/${encodeURIComponent(marketSlug)}`;
|
|
211
|
+
}
|
|
212
|
+
if (marketSlug) return `${POLYMARKET_URL}/event/${encodeURIComponent(marketSlug)}`;
|
|
213
|
+
return POLYMARKET_URL;
|
|
214
|
+
}
|
|
@@ -37,12 +37,14 @@ export async function wrapProvider<T>(
|
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
try {
|
|
40
|
-
const { value: data,
|
|
40
|
+
const { value: data, cache } = await runWithStaleMetadata(fn);
|
|
41
|
+
const timestamp = cache ? new Date(cache.cachedAt).toISOString() : new Date().toISOString();
|
|
41
42
|
return {
|
|
42
43
|
status: "ok",
|
|
43
44
|
data,
|
|
44
|
-
timestamp
|
|
45
|
-
|
|
45
|
+
timestamp,
|
|
46
|
+
cached: cache?.status === "cached" ? true : undefined,
|
|
47
|
+
stale: cache?.status === "stale" ? true : undefined,
|
|
46
48
|
};
|
|
47
49
|
} catch (error) {
|
|
48
50
|
// Credential errors are re-thrown so the tool-layer `withCredentialCheck`
|