gloomberb 0.13.1 → 0.13.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/api-client/index.test.ts +34 -0
- package/src/api-client/request.ts +4 -1
- package/src/cli/pane-functions/cloud-session.test.ts +54 -0
- package/src/cli/pane-functions/cloud-session.ts +38 -0
- package/src/cli/pane-functions/index.ts +5 -1
- package/src/cli/pane-functions/screenshot.ts +2 -19
- package/src/components/command-bar/assist/runtime.ts +38 -30
- package/src/components/command-bar/surface/assist.test.tsx +43 -0
- package/src/news/types.ts +1 -0
- package/src/plugins/builtin/chat/controller/channels.ts +2 -1
- package/src/plugins/builtin/chat/controller/index.ts +13 -5
- package/src/plugins/builtin/chat/controller/realtime.ts +24 -0
- package/src/plugins/builtin/chat/controller/session-runtime.ts +76 -8
- package/src/plugins/builtin/chat/controller/state.ts +1 -0
- package/src/plugins/builtin/chat/controller.test.ts +260 -4
- package/src/plugins/builtin/cloud/askg/executor.test.ts +160 -0
- package/src/plugins/builtin/cloud/askg/executor.ts +429 -0
- package/src/plugins/builtin/cloud/askg/manifest.test.ts +153 -0
- package/src/plugins/builtin/cloud/askg/manifest.ts +314 -0
- package/src/plugins/builtin/cloud/askg/protocol.test.ts +3 -2
- package/src/plugins/builtin/cloud/askg/protocol.ts +16 -4
- package/src/plugins/builtin/cloud/askg/undo.test.ts +44 -0
- package/src/plugins/builtin/cloud/askg/undo.ts +277 -0
- package/src/plugins/builtin/cloud/askg/verify.ts +130 -0
- package/src/plugins/builtin/cloud/plugin.tsx +2 -0
- package/src/plugins/builtin/congress-trades/client.ts +11 -0
- package/src/plugins/builtin/congress-trades/detail.tsx +3 -3
- package/src/plugins/builtin/congress-trades/headless.test.ts +98 -0
- package/src/plugins/builtin/congress-trades/headless.ts +145 -0
- package/src/plugins/builtin/congress-trades/pane.tsx +3 -3
- package/src/plugins/builtin/dividend-yield/headless.test.ts +88 -0
- package/src/plugins/builtin/dividend-yield/headless.ts +135 -0
- package/src/plugins/builtin/dividend-yield/index.tsx +12 -8
- package/src/plugins/builtin/dividend-yield/model.ts +4 -22
- package/src/plugins/builtin/dividend-yield/view.ts +17 -0
- package/src/plugins/builtin/earnings/headless.test.ts +82 -0
- package/src/plugins/builtin/earnings/headless.ts +162 -0
- package/src/plugins/builtin/earnings/index.tsx +2 -0
- package/src/plugins/builtin/earnings-calls/data.ts +29 -8
- package/src/plugins/builtin/earnings-calls/headless.test.ts +192 -0
- package/src/plugins/builtin/earnings-calls/headless.ts +300 -0
- package/src/plugins/builtin/earnings-calls/index.tsx +19 -9
- package/src/plugins/builtin/earnings-calls/model.ts +238 -0
- package/src/plugins/builtin/earnings-calls/transcript-view.tsx +8 -9
- package/src/plugins/builtin/holders/client.ts +29 -0
- package/src/plugins/builtin/holders/headless.test.ts +58 -0
- package/src/plugins/builtin/holders/headless.ts +162 -0
- package/src/plugins/builtin/holders/index.tsx +14 -8
- package/src/plugins/builtin/holders/pane.tsx +7 -1
- package/src/plugins/builtin/insider/client.ts +50 -0
- package/src/plugins/builtin/insider/headless.test.ts +122 -0
- package/src/plugins/builtin/insider/headless.ts +117 -0
- package/src/plugins/builtin/insider/index.tsx +21 -50
- package/src/plugins/builtin/insider/insider-data.ts +1 -1
- package/src/plugins/builtin/insider/model.ts +72 -0
- package/src/plugins/builtin/ipo-calendar/client.ts +15 -0
- package/src/plugins/builtin/ipo-calendar/headless.test.ts +89 -0
- package/src/plugins/builtin/ipo-calendar/headless.ts +127 -0
- package/src/plugins/builtin/ipo-calendar/index.tsx +2 -0
- package/src/plugins/builtin/ipo-calendar/model.ts +3 -15
- package/src/plugins/builtin/news/headless.test.ts +138 -0
- package/src/plugins/builtin/news/headless.ts +190 -0
- package/src/plugins/builtin/news/index.tsx +13 -9
- package/src/plugins/builtin/news/wire/index.tsx +2 -1
- package/src/plugins/builtin/research/analyst-headless.test.ts +60 -0
- package/src/plugins/builtin/research/analyst-headless.ts +172 -0
- package/src/plugins/builtin/research/analyst-model.ts +219 -0
- package/src/plugins/builtin/research/analyst-pane.tsx +31 -221
- package/src/plugins/builtin/research/client.ts +12 -0
- package/src/plugins/builtin/research/headless.test.ts +93 -0
- package/src/plugins/builtin/research/headless.ts +161 -0
- package/src/plugins/builtin/research/index.tsx +28 -18
- package/src/plugins/builtin/sec/client.ts +16 -0
- package/src/plugins/builtin/sec/headless.test.ts +72 -0
- package/src/plugins/builtin/sec/headless.ts +69 -0
- package/src/plugins/builtin/sec/index.tsx +20 -75
- package/src/plugins/builtin/sec/model.ts +89 -0
- package/src/plugins/builtin/short-interest/client.ts +5 -2
- package/src/plugins/builtin/short-interest/headless.test.ts +96 -0
- package/src/plugins/builtin/short-interest/headless.ts +111 -0
- package/src/plugins/builtin/short-interest/index.tsx +15 -9
- package/src/plugins/builtin/thirteenf/data.ts +13 -11
- package/src/plugins/builtin/thirteenf/headless.test.ts +112 -0
- package/src/plugins/builtin/thirteenf/headless.ts +286 -0
- package/src/plugins/builtin/thirteenf/index.tsx +4 -0
- package/src/plugins/builtin/thirteenf/model.ts +13 -0
- package/src/plugins/builtin/thirteenf/table.ts +3 -15
- package/src/plugins/prediction-markets/headless.test.ts +119 -0
- package/src/plugins/prediction-markets/headless.ts +255 -0
- package/src/plugins/prediction-markets/index.tsx +2 -0
- package/src/sources/gloomberb-cloud/news.ts +1 -0
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
import { stableStringify } from "../../../../remote/revision";
|
|
2
|
+
import type {
|
|
3
|
+
RemoteControlRequest,
|
|
4
|
+
RemoteControlResponse,
|
|
5
|
+
RemoteJsonPatchOperation,
|
|
6
|
+
} from "../../../../remote/types";
|
|
7
|
+
import type { JsonValue, ToolResultStatus } from "./protocol";
|
|
8
|
+
|
|
9
|
+
export type InProcessRemoteControlHandler = (
|
|
10
|
+
request: RemoteControlRequest,
|
|
11
|
+
) => Promise<RemoteControlResponse>;
|
|
12
|
+
|
|
13
|
+
interface UndoResourceSnapshot {
|
|
14
|
+
resource: string;
|
|
15
|
+
value: JsonValue;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface StoredUndoResource extends UndoResourceSnapshot {
|
|
19
|
+
expectedRev: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface StoredUndo {
|
|
23
|
+
createdAt: number;
|
|
24
|
+
resources: StoredUndoResource[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface PreparedUndo {
|
|
28
|
+
resources: UndoResourceSnapshot[];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface AppliedUndo {
|
|
32
|
+
status: Extract<ToolResultStatus, "ok" | "error" | "denied" | "cancelled">;
|
|
33
|
+
elapsedMs: number;
|
|
34
|
+
note?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface ASKGUndoManagerOptions {
|
|
38
|
+
maxEntries?: number;
|
|
39
|
+
ttlMs?: number;
|
|
40
|
+
now?: () => number;
|
|
41
|
+
tokenFactory?: () => string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const DEFAULT_MAX_ENTRIES = 50;
|
|
45
|
+
const DEFAULT_TTL_MS = 10 * 60_000;
|
|
46
|
+
|
|
47
|
+
const LAYOUT_UNDO_OPERATIONS = new Set([
|
|
48
|
+
"pane.setSetting",
|
|
49
|
+
"layout.gridlock",
|
|
50
|
+
"layout.closeFloating",
|
|
51
|
+
"layout.placePane",
|
|
52
|
+
"layout.setGrid",
|
|
53
|
+
]);
|
|
54
|
+
|
|
55
|
+
function shortReason(error: unknown): string {
|
|
56
|
+
const text = error instanceof Error ? error.message : String(error);
|
|
57
|
+
return text.replace(/\s+/g, " ").trim().slice(0, 240) || "Unknown error.";
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function normalizeJson(value: unknown, seen = new WeakSet<object>()): JsonValue {
|
|
61
|
+
if (value == null) return null;
|
|
62
|
+
if (typeof value === "string" || typeof value === "boolean") return value;
|
|
63
|
+
if (typeof value === "number") return Number.isFinite(value) ? value : null;
|
|
64
|
+
if (typeof value === "bigint") return value.toString();
|
|
65
|
+
if (value instanceof Date) return value.toISOString();
|
|
66
|
+
if (typeof value !== "object") return String(value);
|
|
67
|
+
if (seen.has(value)) return "[Circular]";
|
|
68
|
+
seen.add(value);
|
|
69
|
+
if (Array.isArray(value)) {
|
|
70
|
+
const result = value.map((entry) => normalizeJson(entry, seen));
|
|
71
|
+
seen.delete(value);
|
|
72
|
+
return result;
|
|
73
|
+
}
|
|
74
|
+
const result: Record<string, JsonValue> = {};
|
|
75
|
+
for (const key of Object.keys(value as Record<string, unknown>).sort()) {
|
|
76
|
+
result[key] = normalizeJson((value as Record<string, unknown>)[key], seen);
|
|
77
|
+
}
|
|
78
|
+
seen.delete(value);
|
|
79
|
+
return result;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function paneStateResource(args: Record<string, JsonValue>): string | null {
|
|
83
|
+
const paneId = args.paneId;
|
|
84
|
+
return typeof paneId === "string" && paneId.length > 0
|
|
85
|
+
? `app://pane-state/${encodeURIComponent(paneId)}`
|
|
86
|
+
: null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function resourcesForOperation(
|
|
90
|
+
operation: string,
|
|
91
|
+
args: Record<string, JsonValue>,
|
|
92
|
+
): string[] | null {
|
|
93
|
+
if (operation === "pane.setState") {
|
|
94
|
+
const resource = paneStateResource(args);
|
|
95
|
+
return resource ? [resource] : null;
|
|
96
|
+
}
|
|
97
|
+
if (LAYOUT_UNDO_OPERATIONS.has(operation)) return ["app://layout/current"];
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function pointerSegment(value: string): string {
|
|
102
|
+
return value.replace(/~/g, "~0").replace(/\//g, "~1");
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function isObject(value: JsonValue): value is Record<string, JsonValue> {
|
|
106
|
+
return value != null && typeof value === "object" && !Array.isArray(value);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function replacementPatch(current: JsonValue, target: JsonValue): RemoteJsonPatchOperation[] | null {
|
|
110
|
+
if (!isObject(current) || !isObject(target)) return null;
|
|
111
|
+
const patch: RemoteJsonPatchOperation[] = [];
|
|
112
|
+
const currentKeys = Object.keys(current).sort();
|
|
113
|
+
const targetKeys = Object.keys(target).sort();
|
|
114
|
+
for (const key of currentKeys) {
|
|
115
|
+
if (!(key in target)) patch.push({ op: "remove", path: `/${pointerSegment(key)}` });
|
|
116
|
+
}
|
|
117
|
+
for (const key of targetKeys) {
|
|
118
|
+
const path = `/${pointerSegment(key)}`;
|
|
119
|
+
if (!(key in current)) {
|
|
120
|
+
patch.push({ op: "add", path, value: target[key] });
|
|
121
|
+
} else if (stableStringify(current[key]) !== stableStringify(target[key])) {
|
|
122
|
+
patch.push({ op: "replace", path, value: target[key] });
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return patch;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function throwIfCancelled(signal?: AbortSignal): void {
|
|
129
|
+
if (!signal?.aborted) return;
|
|
130
|
+
throw signal.reason instanceof Error ? signal.reason : new Error("Undo was cancelled.");
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function randomToken(): string {
|
|
134
|
+
return globalThis.crypto.randomUUID();
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export class ASKGUndoManager {
|
|
138
|
+
private readonly entries = new Map<string, StoredUndo>();
|
|
139
|
+
private readonly maxEntries: number;
|
|
140
|
+
private readonly ttlMs: number;
|
|
141
|
+
private readonly now: () => number;
|
|
142
|
+
private readonly tokenFactory: () => string;
|
|
143
|
+
|
|
144
|
+
constructor(
|
|
145
|
+
private readonly remoteHandler: InProcessRemoteControlHandler,
|
|
146
|
+
options: ASKGUndoManagerOptions = {},
|
|
147
|
+
) {
|
|
148
|
+
this.maxEntries = options.maxEntries ?? DEFAULT_MAX_ENTRIES;
|
|
149
|
+
this.ttlMs = options.ttlMs ?? DEFAULT_TTL_MS;
|
|
150
|
+
this.now = options.now ?? Date.now;
|
|
151
|
+
this.tokenFactory = options.tokenFactory ?? randomToken;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async prepare(
|
|
155
|
+
operation: string,
|
|
156
|
+
args: Record<string, JsonValue>,
|
|
157
|
+
signal?: AbortSignal,
|
|
158
|
+
): Promise<PreparedUndo | null> {
|
|
159
|
+
const resources = resourcesForOperation(operation, args);
|
|
160
|
+
if (!resources) return null;
|
|
161
|
+
const snapshots: UndoResourceSnapshot[] = [];
|
|
162
|
+
for (const resource of resources) {
|
|
163
|
+
throwIfCancelled(signal);
|
|
164
|
+
const response = await this.remoteHandler({ type: "get", resource });
|
|
165
|
+
if (!response.ok) throw new Error(response.error.message);
|
|
166
|
+
snapshots.push({ resource, value: normalizeJson(response.data) });
|
|
167
|
+
}
|
|
168
|
+
return { resources: snapshots };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async commit(prepared: PreparedUndo, signal?: AbortSignal): Promise<string | null> {
|
|
172
|
+
const resources: StoredUndoResource[] = [];
|
|
173
|
+
let changed = false;
|
|
174
|
+
for (const before of prepared.resources) {
|
|
175
|
+
throwIfCancelled(signal);
|
|
176
|
+
const response = await this.remoteHandler({ type: "get", resource: before.resource });
|
|
177
|
+
if (!response.ok) throw new Error(response.error.message);
|
|
178
|
+
if (!response.rev) throw new Error(`Resource "${before.resource}" did not return a revision.`);
|
|
179
|
+
const current = normalizeJson(response.data);
|
|
180
|
+
if (stableStringify(current) !== stableStringify(before.value)) changed = true;
|
|
181
|
+
resources.push({ ...before, expectedRev: response.rev });
|
|
182
|
+
}
|
|
183
|
+
if (!changed) return null;
|
|
184
|
+
|
|
185
|
+
this.prune();
|
|
186
|
+
const token = this.tokenFactory();
|
|
187
|
+
this.entries.set(token, { createdAt: this.now(), resources });
|
|
188
|
+
while (this.entries.size > this.maxEntries) {
|
|
189
|
+
const oldest = this.entries.keys().next().value as string | undefined;
|
|
190
|
+
if (!oldest) break;
|
|
191
|
+
this.entries.delete(oldest);
|
|
192
|
+
}
|
|
193
|
+
return token;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async undo(token: string, signal?: AbortSignal): Promise<AppliedUndo> {
|
|
197
|
+
const startedAt = this.now();
|
|
198
|
+
this.prune();
|
|
199
|
+
const entry = this.entries.get(token);
|
|
200
|
+
if (!entry) {
|
|
201
|
+
return {
|
|
202
|
+
status: "denied",
|
|
203
|
+
elapsedMs: Math.max(0, this.now() - startedAt),
|
|
204
|
+
note: "Undo token is unknown or expired.",
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
try {
|
|
209
|
+
const currentResources: Array<{
|
|
210
|
+
snapshot: StoredUndoResource;
|
|
211
|
+
value: JsonValue;
|
|
212
|
+
rev: string;
|
|
213
|
+
patch: RemoteJsonPatchOperation[];
|
|
214
|
+
}> = [];
|
|
215
|
+
for (const snapshot of entry.resources) {
|
|
216
|
+
throwIfCancelled(signal);
|
|
217
|
+
const response = await this.remoteHandler({ type: "get", resource: snapshot.resource });
|
|
218
|
+
if (!response.ok) throw new Error(response.error.message);
|
|
219
|
+
if (!response.rev) throw new Error(`Resource "${snapshot.resource}" did not return a revision.`);
|
|
220
|
+
if (response.rev !== snapshot.expectedRev) {
|
|
221
|
+
return {
|
|
222
|
+
status: "denied",
|
|
223
|
+
elapsedMs: Math.max(0, this.now() - startedAt),
|
|
224
|
+
note: `Cannot undo because "${snapshot.resource}" changed after the tool call.`,
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
const value = normalizeJson(response.data);
|
|
228
|
+
const patch = replacementPatch(value, snapshot.value);
|
|
229
|
+
if (!patch) {
|
|
230
|
+
return {
|
|
231
|
+
status: "denied",
|
|
232
|
+
elapsedMs: Math.max(0, this.now() - startedAt),
|
|
233
|
+
note: `Cannot safely restore non-object resource "${snapshot.resource}".`,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
currentResources.push({ snapshot, value, rev: response.rev, patch });
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
for (const current of currentResources) {
|
|
240
|
+
throwIfCancelled(signal);
|
|
241
|
+
if (current.patch.length === 0) continue;
|
|
242
|
+
const response = await this.remoteHandler({
|
|
243
|
+
type: "patch",
|
|
244
|
+
resource: current.snapshot.resource,
|
|
245
|
+
patch: current.patch,
|
|
246
|
+
expectRev: current.rev,
|
|
247
|
+
});
|
|
248
|
+
if (!response.ok) throw new Error(response.error.message);
|
|
249
|
+
}
|
|
250
|
+
this.entries.delete(token);
|
|
251
|
+
return {
|
|
252
|
+
status: "ok",
|
|
253
|
+
elapsedMs: Math.max(0, this.now() - startedAt),
|
|
254
|
+
};
|
|
255
|
+
} catch (error) {
|
|
256
|
+
if (signal?.aborted) {
|
|
257
|
+
return {
|
|
258
|
+
status: "cancelled",
|
|
259
|
+
elapsedMs: Math.max(0, this.now() - startedAt),
|
|
260
|
+
note: "Undo was cancelled.",
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
return {
|
|
264
|
+
status: "error",
|
|
265
|
+
elapsedMs: Math.max(0, this.now() - startedAt),
|
|
266
|
+
note: shortReason(error),
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
private prune(): void {
|
|
272
|
+
const cutoff = this.now() - this.ttlMs;
|
|
273
|
+
for (const [token, entry] of this.entries) {
|
|
274
|
+
if (entry.createdAt < cutoff) this.entries.delete(token);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { createAppServices } from "../../../../core/app-services";
|
|
5
|
+
import {
|
|
6
|
+
appReducer,
|
|
7
|
+
createInitialState,
|
|
8
|
+
type AppAction,
|
|
9
|
+
} from "../../../../core/state/app/state";
|
|
10
|
+
import { getLoadablePlugins } from "../../../catalog";
|
|
11
|
+
import { createAppRemoteController } from "../../../../remote/controller";
|
|
12
|
+
import type { RemoteUiRegistry } from "../../../../remote/semantic-tree";
|
|
13
|
+
import { createDefaultConfig } from "../../../../types/config";
|
|
14
|
+
import type { ASKGToolCallEvent, ClientToolManifest } from "./protocol";
|
|
15
|
+
import { createASKGToolExecutor } from "./executor";
|
|
16
|
+
import { buildASKGClientManifest, REMOTE_RESOURCE_TOOL_NAME } from "./manifest";
|
|
17
|
+
|
|
18
|
+
function toolCall(tool: ClientToolManifest, args: ASKGToolCallEvent["args"]): ASKGToolCallEvent {
|
|
19
|
+
return {
|
|
20
|
+
seq: 1,
|
|
21
|
+
type: "tool-call",
|
|
22
|
+
turnId: "verify-turn",
|
|
23
|
+
toolCallId: `verify-${tool.name}`,
|
|
24
|
+
name: tool.name,
|
|
25
|
+
args,
|
|
26
|
+
writeTier: tool.writeTier,
|
|
27
|
+
requiresConfirmation: false,
|
|
28
|
+
preview: null,
|
|
29
|
+
timeoutMs: tool.timeoutMs,
|
|
30
|
+
expiresAt: new Date(Date.now() + tool.timeoutMs).toISOString(),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function resultShape(value: unknown): string {
|
|
35
|
+
if (Array.isArray(value)) return "array";
|
|
36
|
+
if (value == null) return String(value);
|
|
37
|
+
if (typeof value !== "object") return typeof value;
|
|
38
|
+
return Object.keys(value as Record<string, unknown>).sort().join(",") || "object";
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function verify() {
|
|
42
|
+
const dataDir = mkdtempSync(join(tmpdir(), "gloomberb-askg-client-"));
|
|
43
|
+
const config = { ...createDefaultConfig(dataDir), onboardingComplete: true };
|
|
44
|
+
const services = createAppServices({ config, plugins: getLoadablePlugins() });
|
|
45
|
+
try {
|
|
46
|
+
await services.ready;
|
|
47
|
+
let state = createInitialState(config);
|
|
48
|
+
const dispatch = (action: AppAction) => {
|
|
49
|
+
state = appReducer(state, action);
|
|
50
|
+
};
|
|
51
|
+
services.pluginRegistry.getConfigFn = () => state.config;
|
|
52
|
+
services.pluginRegistry.getLayoutFn = () => state.config.layout;
|
|
53
|
+
services.pluginRegistry.updateLayoutFn = (layout) => dispatch({ type: "UPDATE_LAYOUT", layout });
|
|
54
|
+
|
|
55
|
+
const uiRegistry: RemoteUiRegistry = {
|
|
56
|
+
register() {},
|
|
57
|
+
unregister() {},
|
|
58
|
+
snapshot: () => [],
|
|
59
|
+
invoke: async () => null,
|
|
60
|
+
};
|
|
61
|
+
const remote = createAppRemoteController({
|
|
62
|
+
dispatch,
|
|
63
|
+
getState: () => state,
|
|
64
|
+
pluginRegistry: services.pluginRegistry,
|
|
65
|
+
uiRegistry,
|
|
66
|
+
});
|
|
67
|
+
const built = await buildASKGClientManifest(services.pluginRegistry);
|
|
68
|
+
const sources = { headless: 0, "remote-op": 0 };
|
|
69
|
+
const tiers = { read: 0, "ui-write": 0, "user-data": 0, broker: 0 };
|
|
70
|
+
for (const tool of built.tools) {
|
|
71
|
+
sources[tool.source] += 1;
|
|
72
|
+
tiers[tool.writeTier] += 1;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
console.log(JSON.stringify({
|
|
76
|
+
totalTools: built.tools.length,
|
|
77
|
+
sources,
|
|
78
|
+
tiers,
|
|
79
|
+
headlessNames: built.tools.filter(({ source }) => source === "headless").map(({ name }) => name),
|
|
80
|
+
manifestHash: built.manifestHash,
|
|
81
|
+
skipped: built.skipped,
|
|
82
|
+
}, null, 2));
|
|
83
|
+
|
|
84
|
+
const executor = createASKGToolExecutor({
|
|
85
|
+
manifests: built.tools,
|
|
86
|
+
registry: services.pluginRegistry,
|
|
87
|
+
context: {
|
|
88
|
+
config,
|
|
89
|
+
dataDir,
|
|
90
|
+
persistence: services.persistence,
|
|
91
|
+
store: services.tickerRepository,
|
|
92
|
+
dataProvider: services.providerRouter,
|
|
93
|
+
},
|
|
94
|
+
remoteHandler: remote.handle,
|
|
95
|
+
});
|
|
96
|
+
const headless = built.tools.find(({ source, name, argument }) => (
|
|
97
|
+
source === "headless" && name === "val" && (argument?.kind === "none" || argument?.optional)
|
|
98
|
+
)) ?? built.tools.find(({ source, argument }) => (
|
|
99
|
+
source === "headless" && (argument?.kind === "none" || argument?.optional)
|
|
100
|
+
));
|
|
101
|
+
const resource = built.tools.find(({ name }) => name === REMOTE_RESOURCE_TOOL_NAME);
|
|
102
|
+
if (!headless || !resource) throw new Error("Verification tools are missing from the manifest.");
|
|
103
|
+
|
|
104
|
+
const headlessResult = await executor.execute(toolCall(headless, {}));
|
|
105
|
+
const remoteResult = await executor.execute(toolCall(resource, { resource: "app://snapshot" }));
|
|
106
|
+
console.log(JSON.stringify({
|
|
107
|
+
headless: {
|
|
108
|
+
name: headless.name,
|
|
109
|
+
status: headlessResult.status,
|
|
110
|
+
resultShape: resultShape(headlessResult.result),
|
|
111
|
+
rowCount: headlessResult.rowCount,
|
|
112
|
+
elapsedMs: headlessResult.elapsedMs,
|
|
113
|
+
},
|
|
114
|
+
remote: {
|
|
115
|
+
name: resource.name,
|
|
116
|
+
status: remoteResult.status,
|
|
117
|
+
resultShape: resultShape(remoteResult.result),
|
|
118
|
+
rowCount: remoteResult.rowCount,
|
|
119
|
+
elapsedMs: remoteResult.elapsedMs,
|
|
120
|
+
},
|
|
121
|
+
}, null, 2));
|
|
122
|
+
} finally {
|
|
123
|
+
services.destroy();
|
|
124
|
+
rmSync(dataDir, { recursive: true, force: true });
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (import.meta.main) {
|
|
129
|
+
await verify();
|
|
130
|
+
}
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
CONGRESS_TRADES_PANE_ID,
|
|
18
18
|
CongressTradesPane,
|
|
19
19
|
} from "../congress-trades/pane";
|
|
20
|
+
import { congressHeadless } from "../congress-trades/headless";
|
|
20
21
|
import { registerTwitterFeedFeature } from "../cloud-tweets/registration";
|
|
21
22
|
import { composeBuiltinPlugin, type PluginModule } from "../plugin-module";
|
|
22
23
|
import { registerCloudAuthCommands } from "./auth-commands";
|
|
@@ -181,6 +182,7 @@ const congressTradesModule: PluginModule = {
|
|
|
181
182
|
description: "Track newly disclosed House periodic transaction reports.",
|
|
182
183
|
keywords: ["congress", "house", "trades", "ptr", "stock", "disclosures"],
|
|
183
184
|
shortcut: { prefix: "CG" },
|
|
185
|
+
headless: congressHeadless,
|
|
184
186
|
createInstance: () => ({ placement: "floating" }),
|
|
185
187
|
publicShare: createPublicPaneShare("Congress Trades"),
|
|
186
188
|
}],
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { apiClient } from "../../../api-client";
|
|
2
|
+
import type { CloudCongressHousePayload } from "../../../api-client";
|
|
3
|
+
import type { CloudCongressHouseParams } from "../../../api-client/paths";
|
|
4
|
+
import type { HeadlessPaneApiClient } from "../../../types/plugin";
|
|
5
|
+
|
|
6
|
+
export function loadCongressHouse(
|
|
7
|
+
params: CloudCongressHouseParams = {},
|
|
8
|
+
client: Pick<HeadlessPaneApiClient, "getCloudCongressHouse"> = apiClient,
|
|
9
|
+
): Promise<CloudCongressHousePayload> {
|
|
10
|
+
return client.getCloudCongressHouse(params);
|
|
11
|
+
}
|
|
@@ -15,7 +15,6 @@ import type {
|
|
|
15
15
|
CloudCongressMemberPayload,
|
|
16
16
|
CloudCongressTradePayload,
|
|
17
17
|
} from "../../../api-client";
|
|
18
|
-
import { apiClient } from "../../../api-client";
|
|
19
18
|
import {
|
|
20
19
|
CONGRESS_MEMBER_FILING_LIMIT,
|
|
21
20
|
CONGRESS_MEMBER_TRADE_LIMIT,
|
|
@@ -34,6 +33,7 @@ import {
|
|
|
34
33
|
type TradeColumnId,
|
|
35
34
|
} from "./model";
|
|
36
35
|
import { renderCongressTradeCell } from "./table";
|
|
36
|
+
import { loadCongressHouse } from "./client";
|
|
37
37
|
|
|
38
38
|
function DetailLine({
|
|
39
39
|
label,
|
|
@@ -133,7 +133,7 @@ export function MemberTradesDetail({
|
|
|
133
133
|
const gen = fetchGenRef.current;
|
|
134
134
|
setStatus("loading");
|
|
135
135
|
setError(null);
|
|
136
|
-
|
|
136
|
+
loadCongressHouse({
|
|
137
137
|
member: member.memberName,
|
|
138
138
|
limit: CONGRESS_MEMBER_TRADE_LIMIT,
|
|
139
139
|
filingLimit: Math.max(CONGRESS_MEMBER_FILING_LIMIT, filingLimit),
|
|
@@ -162,7 +162,7 @@ export function MemberTradesDetail({
|
|
|
162
162
|
if (!nextRequest) return;
|
|
163
163
|
const gen = fetchGenRef.current;
|
|
164
164
|
setLoadingMore(true);
|
|
165
|
-
|
|
165
|
+
loadCongressHouse({
|
|
166
166
|
...nextRequest,
|
|
167
167
|
member: member.memberName,
|
|
168
168
|
limit: CONGRESS_MEMBER_TRADE_LIMIT,
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import type { CloudCongressHousePayload } from "../../../api-client";
|
|
3
|
+
import { createTestDataProvider } from "../../../test-support/data-provider";
|
|
4
|
+
import { createDefaultConfig } from "../../../types/config";
|
|
5
|
+
import type { HeadlessPaneContext, HeadlessPaneLoadArgs } from "../../../types/plugin";
|
|
6
|
+
import { createCongressHeadless } from "./headless";
|
|
7
|
+
|
|
8
|
+
const payload: CloudCongressHousePayload = {
|
|
9
|
+
asOf: "2026-09-04T12:00:00.000Z",
|
|
10
|
+
chamber: "house",
|
|
11
|
+
source: "house-clerk",
|
|
12
|
+
year: 2026,
|
|
13
|
+
indexUpdatedAt: "2026-09-04T11:00:00.000Z",
|
|
14
|
+
filingsScanned: 2,
|
|
15
|
+
filingCount: 2,
|
|
16
|
+
trades: [
|
|
17
|
+
{
|
|
18
|
+
id: "trade-1",
|
|
19
|
+
chamber: "house",
|
|
20
|
+
filingId: "filing-1",
|
|
21
|
+
docId: "doc-1",
|
|
22
|
+
memberName: "Nancy Pelosi",
|
|
23
|
+
stateDistrict: "CA-11",
|
|
24
|
+
filingDate: "2026-09-03",
|
|
25
|
+
transactionDate: "2026-08-20",
|
|
26
|
+
notificationDate: null,
|
|
27
|
+
lagDays: 14,
|
|
28
|
+
side: "BUY",
|
|
29
|
+
transactionType: "Purchase",
|
|
30
|
+
ticker: "NVDA",
|
|
31
|
+
assetName: "NVIDIA Corporation",
|
|
32
|
+
assetType: "Stock",
|
|
33
|
+
owner: "SP",
|
|
34
|
+
rawOwner: "Spouse",
|
|
35
|
+
amount: "$1,000,001 - $5,000,000",
|
|
36
|
+
amountLow: 1_000_001,
|
|
37
|
+
amountHigh: 5_000_000,
|
|
38
|
+
capGainsOver200: null,
|
|
39
|
+
filingStatus: null,
|
|
40
|
+
subholdingOf: null,
|
|
41
|
+
description: null,
|
|
42
|
+
sourceUrl: "https://disclosures-clerk.house.gov/filing-1",
|
|
43
|
+
},
|
|
44
|
+
],
|
|
45
|
+
members: [
|
|
46
|
+
{
|
|
47
|
+
id: "member-1",
|
|
48
|
+
memberName: "Nancy Pelosi",
|
|
49
|
+
stateDistrict: "CA-11",
|
|
50
|
+
tradeCount: 12,
|
|
51
|
+
buyCount: 8,
|
|
52
|
+
sellCount: 4,
|
|
53
|
+
exchangeCount: 0,
|
|
54
|
+
otherCount: 0,
|
|
55
|
+
estimatedLow: 2_000_000,
|
|
56
|
+
estimatedHigh: 8_000_000,
|
|
57
|
+
lastFilingDate: "2026-09-03",
|
|
58
|
+
avgLagDays: 18,
|
|
59
|
+
},
|
|
60
|
+
],
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
function context(): HeadlessPaneContext {
|
|
64
|
+
return {
|
|
65
|
+
marketData: createTestDataProvider(),
|
|
66
|
+
apiClient: {} as HeadlessPaneContext["apiClient"],
|
|
67
|
+
config: createDefaultConfig("/tmp/gloomberb-headless-congress"),
|
|
68
|
+
signal: new AbortController().signal,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function args(tab: "trades" | "members", limit = 50): HeadlessPaneLoadArgs {
|
|
73
|
+
return { rawArgument: "", argument: null, symbols: [], options: { tab, year: 2026, limit } };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
describe("congress headless model", () => {
|
|
77
|
+
test("projects the active tab and applies its row limit", async () => {
|
|
78
|
+
const headless = createCongressHeadless({ loadHouse: async () => payload });
|
|
79
|
+
|
|
80
|
+
const trades = await headless.load(args("trades", 1), context());
|
|
81
|
+
expect(trades.rows).toMatchObject([{
|
|
82
|
+
memberName: "Nancy Pelosi",
|
|
83
|
+
side: "BUY",
|
|
84
|
+
ticker: "NVDA",
|
|
85
|
+
amountLow: 1_000_001,
|
|
86
|
+
amountHigh: 5_000_000,
|
|
87
|
+
}]);
|
|
88
|
+
expect(trades.columns?.map((column) => column.key)).toContain("ticker");
|
|
89
|
+
|
|
90
|
+
const members = await headless.load(args("members", 1), context());
|
|
91
|
+
expect(members.rows).toMatchObject([{
|
|
92
|
+
memberName: "Nancy Pelosi",
|
|
93
|
+
tradeCount: 12,
|
|
94
|
+
buyCount: 8,
|
|
95
|
+
}]);
|
|
96
|
+
expect(members.columns?.map((column) => column.key)).toContain("tradeCount");
|
|
97
|
+
});
|
|
98
|
+
});
|