gloomberb 0.12.0 → 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.
Files changed (219) hide show
  1. package/README.md +2 -0
  2. package/package.json +1 -2
  3. package/src/api-client/auth.ts +2 -1
  4. package/src/api-client/index.test.ts +34 -0
  5. package/src/api-client/index.ts +10 -2
  6. package/src/api-client/request.ts +4 -1
  7. package/src/api-client/research-activity.ts +61 -0
  8. package/src/api-client/types.ts +2 -1
  9. package/src/cli/commands/plugins.ts +4 -3
  10. package/src/cli/desktop-pane-shot.ts +79 -10
  11. package/src/cli/pane-functions/capabilities.ts +76 -23
  12. package/src/cli/pane-functions/cloud-session.test.ts +54 -0
  13. package/src/cli/pane-functions/cloud-session.ts +38 -0
  14. package/src/cli/pane-functions/dom.test.ts +41 -0
  15. package/src/cli/pane-functions/dom.ts +199 -0
  16. package/src/cli/pane-functions/index.test.ts +13 -2
  17. package/src/cli/pane-functions/index.ts +9 -5
  18. package/src/cli/pane-functions/intraday-shot.test.ts +307 -0
  19. package/src/cli/pane-functions/intraday-shot.ts +334 -0
  20. package/src/cli/pane-functions/report.test.ts +29 -1
  21. package/src/cli/pane-functions/report.ts +68 -7
  22. package/src/cli/pane-functions/screenshot.ts +195 -42
  23. package/src/cli/restore-plugins.ts +10 -3
  24. package/src/components/chart/composite/composite-chart.test.tsx +68 -1
  25. package/src/components/chart/composite/composite-chart.tsx +87 -21
  26. package/src/components/chart/composite/rasterizer.ts +33 -0
  27. package/src/components/chart/composite/renderers.test.ts +51 -4
  28. package/src/components/chart/composite/scene.test.ts +128 -2
  29. package/src/components/chart/composite/scene.ts +92 -16
  30. package/src/components/chart/composite/text-renderer.ts +13 -0
  31. package/src/components/chart/composite/time-scale.ts +30 -10
  32. package/src/components/chart/composite/types.ts +26 -0
  33. package/src/components/command-bar/assist/runtime.ts +38 -30
  34. package/src/components/command-bar/surface/assist.test.tsx +63 -9
  35. package/src/components/command-bar/surface/test-harness.tsx +23 -4
  36. package/src/components/onboarding/account-step/chooser-panel.tsx +2 -2
  37. package/src/components/onboarding/account-step/qr-panel.tsx +2 -10
  38. package/src/components/onboarding/onboarding-wizard.test.tsx +38 -12
  39. package/src/components/onboarding/onboarding-wizard.tsx +49 -14
  40. package/src/data/config/store/index.test.ts +23 -0
  41. package/src/data/config/store/node.ts +7 -1
  42. package/src/data/config/store/normalize.ts +4 -0
  43. package/src/news/types.ts +1 -0
  44. package/src/plugins/builtin/cds/client.test.ts +20 -0
  45. package/src/plugins/builtin/cds/client.ts +13 -3
  46. package/src/plugins/builtin/cds/headless.test.ts +30 -0
  47. package/src/plugins/builtin/cds/headless.ts +113 -0
  48. package/src/plugins/builtin/cds/index.tsx +4 -0
  49. package/src/plugins/builtin/chart-composer/cli-options.ts +4 -1
  50. package/src/plugins/builtin/chart-composer/index.tsx +1 -1
  51. package/src/plugins/builtin/chat/controller/channels.ts +2 -1
  52. package/src/plugins/builtin/chat/controller/index.ts +13 -5
  53. package/src/plugins/builtin/chat/controller/realtime.ts +24 -0
  54. package/src/plugins/builtin/chat/controller/session-runtime.ts +76 -8
  55. package/src/plugins/builtin/chat/controller/state.ts +1 -0
  56. package/src/plugins/builtin/chat/controller.test.ts +260 -4
  57. package/src/plugins/builtin/cloud/askg/executor.test.ts +160 -0
  58. package/src/plugins/builtin/cloud/askg/executor.ts +429 -0
  59. package/src/plugins/builtin/cloud/askg/manifest.test.ts +153 -0
  60. package/src/plugins/builtin/cloud/askg/manifest.ts +314 -0
  61. package/src/plugins/builtin/cloud/askg/protocol.test.ts +190 -0
  62. package/src/plugins/builtin/cloud/askg/protocol.ts +286 -0
  63. package/src/plugins/builtin/cloud/askg/undo.test.ts +44 -0
  64. package/src/plugins/builtin/cloud/askg/undo.ts +277 -0
  65. package/src/plugins/builtin/cloud/askg/verify.ts +130 -0
  66. package/src/plugins/builtin/cloud/auth-dialog.tsx +7 -1
  67. package/src/plugins/builtin/cloud/device-signin-dialog.tsx +21 -8
  68. package/src/plugins/builtin/cloud/plugin.tsx +2 -0
  69. package/src/plugins/builtin/cloud/verification-panel.tsx +56 -0
  70. package/src/plugins/builtin/congress-trades/client.ts +11 -0
  71. package/src/plugins/builtin/congress-trades/detail.tsx +3 -3
  72. package/src/plugins/builtin/congress-trades/headless.test.ts +98 -0
  73. package/src/plugins/builtin/congress-trades/headless.ts +145 -0
  74. package/src/plugins/builtin/congress-trades/pane.tsx +3 -3
  75. package/src/plugins/builtin/credit-conditions/client.ts +1 -1
  76. package/src/plugins/builtin/credit-conditions/headless.test.ts +31 -0
  77. package/src/plugins/builtin/credit-conditions/headless.ts +55 -0
  78. package/src/plugins/builtin/credit-conditions/index.tsx +4 -0
  79. package/src/plugins/builtin/dividend-yield/headless.test.ts +88 -0
  80. package/src/plugins/builtin/dividend-yield/headless.ts +135 -0
  81. package/src/plugins/builtin/dividend-yield/index.tsx +12 -8
  82. package/src/plugins/builtin/dividend-yield/model.ts +4 -22
  83. package/src/plugins/builtin/dividend-yield/view.ts +17 -0
  84. package/src/plugins/builtin/earnings/headless.test.ts +82 -0
  85. package/src/plugins/builtin/earnings/headless.ts +162 -0
  86. package/src/plugins/builtin/earnings/index.tsx +2 -0
  87. package/src/plugins/builtin/earnings-calls/data.ts +29 -8
  88. package/src/plugins/builtin/earnings-calls/headless.test.ts +192 -0
  89. package/src/plugins/builtin/earnings-calls/headless.ts +300 -0
  90. package/src/plugins/builtin/earnings-calls/index.tsx +19 -9
  91. package/src/plugins/builtin/earnings-calls/model.ts +238 -0
  92. package/src/plugins/builtin/earnings-calls/pane.tsx +4 -0
  93. package/src/plugins/builtin/earnings-calls/transcript-view.tsx +13 -11
  94. package/src/plugins/builtin/fear-greed/headless.test.ts +53 -0
  95. package/src/plugins/builtin/fear-greed/headless.ts +84 -0
  96. package/src/plugins/builtin/fear-greed/index.tsx +4 -0
  97. package/src/plugins/builtin/holders/client.ts +29 -0
  98. package/src/plugins/builtin/holders/headless.test.ts +58 -0
  99. package/src/plugins/builtin/holders/headless.ts +162 -0
  100. package/src/plugins/builtin/holders/index.tsx +14 -8
  101. package/src/plugins/builtin/holders/pane.tsx +7 -1
  102. package/src/plugins/builtin/insider/client.ts +50 -0
  103. package/src/plugins/builtin/insider/headless.test.ts +122 -0
  104. package/src/plugins/builtin/insider/headless.ts +117 -0
  105. package/src/plugins/builtin/insider/index.tsx +21 -50
  106. package/src/plugins/builtin/insider/insider-data.ts +1 -1
  107. package/src/plugins/builtin/insider/model.ts +72 -0
  108. package/src/plugins/builtin/ipo-calendar/client.ts +15 -0
  109. package/src/plugins/builtin/ipo-calendar/headless.test.ts +89 -0
  110. package/src/plugins/builtin/ipo-calendar/headless.ts +127 -0
  111. package/src/plugins/builtin/ipo-calendar/index.tsx +2 -0
  112. package/src/plugins/builtin/ipo-calendar/model.ts +3 -15
  113. package/src/plugins/builtin/market-movers/client.ts +76 -0
  114. package/src/plugins/builtin/market-movers/headless.test.ts +34 -0
  115. package/src/plugins/builtin/market-movers/headless.ts +73 -0
  116. package/src/plugins/builtin/market-movers/index.tsx +11 -51
  117. package/src/plugins/builtin/news/headless.test.ts +138 -0
  118. package/src/plugins/builtin/news/headless.ts +190 -0
  119. package/src/plugins/builtin/news/index.tsx +13 -9
  120. package/src/plugins/builtin/news/wire/index.tsx +2 -1
  121. package/src/plugins/builtin/research/analyst-headless.test.ts +60 -0
  122. package/src/plugins/builtin/research/analyst-headless.ts +172 -0
  123. package/src/plugins/builtin/research/analyst-model.ts +219 -0
  124. package/src/plugins/builtin/research/analyst-pane.tsx +31 -221
  125. package/src/plugins/builtin/research/client.ts +12 -0
  126. package/src/plugins/builtin/research/corporate-actions-pane.tsx +3 -237
  127. package/src/plugins/builtin/research/event-model.ts +245 -0
  128. package/src/plugins/builtin/research/events-headless.test.ts +32 -0
  129. package/src/plugins/builtin/research/events-headless.ts +87 -0
  130. package/src/plugins/builtin/research/headless.test.ts +93 -0
  131. package/src/plugins/builtin/research/headless.ts +161 -0
  132. package/src/plugins/builtin/research/index.tsx +32 -18
  133. package/src/plugins/builtin/research-search/pane.tsx +4 -0
  134. package/src/plugins/builtin/scanner/headless.test.ts +59 -0
  135. package/src/plugins/builtin/scanner/headless.ts +125 -0
  136. package/src/plugins/builtin/scanner/index.ts +4 -0
  137. package/src/plugins/builtin/sec/client.ts +16 -0
  138. package/src/plugins/builtin/sec/headless.test.ts +72 -0
  139. package/src/plugins/builtin/sec/headless.ts +69 -0
  140. package/src/plugins/builtin/sec/index.tsx +20 -75
  141. package/src/plugins/builtin/sec/model.ts +89 -0
  142. package/src/plugins/builtin/sectors/client.ts +54 -0
  143. package/src/plugins/builtin/sectors/headless.test.ts +28 -0
  144. package/src/plugins/builtin/sectors/headless.ts +80 -0
  145. package/src/plugins/builtin/sectors/index.tsx +6 -51
  146. package/src/plugins/builtin/shared/cloud-upgrade.ts +8 -4
  147. package/src/plugins/builtin/short-interest/client.ts +5 -2
  148. package/src/plugins/builtin/short-interest/headless.test.ts +96 -0
  149. package/src/plugins/builtin/short-interest/headless.ts +111 -0
  150. package/src/plugins/builtin/short-interest/index.tsx +15 -9
  151. package/src/plugins/builtin/thirteenf/data.ts +13 -11
  152. package/src/plugins/builtin/thirteenf/headless.test.ts +112 -0
  153. package/src/plugins/builtin/thirteenf/headless.ts +286 -0
  154. package/src/plugins/builtin/thirteenf/index.tsx +4 -0
  155. package/src/plugins/builtin/thirteenf/model.ts +13 -0
  156. package/src/plugins/builtin/thirteenf/table.ts +3 -15
  157. package/src/plugins/builtin/ticker-detail/pane.tsx +14 -0
  158. package/src/plugins/builtin/treasury-auctions/headless.test.ts +34 -0
  159. package/src/plugins/builtin/treasury-auctions/headless.ts +95 -0
  160. package/src/plugins/builtin/treasury-auctions/index.tsx +4 -0
  161. package/src/plugins/builtin/volatility/client.ts +16 -3
  162. package/src/plugins/builtin/volatility/headless.test.ts +35 -0
  163. package/src/plugins/builtin/volatility/headless.ts +75 -0
  164. package/src/plugins/builtin/volatility/index.tsx +4 -0
  165. package/src/plugins/builtin/world-indices/client.ts +43 -0
  166. package/src/plugins/builtin/world-indices/headless.test.ts +32 -0
  167. package/src/plugins/builtin/world-indices/headless.ts +109 -0
  168. package/src/plugins/builtin/world-indices/index.tsx +4 -0
  169. package/src/plugins/builtin/yield-curve/headless.test.ts +24 -0
  170. package/src/plugins/builtin/yield-curve/headless.ts +67 -0
  171. package/src/plugins/builtin/yield-curve/index.tsx +6 -8
  172. package/src/plugins/builtin/yield-curve/treasury-data.ts +16 -6
  173. package/src/plugins/bundle.test.ts +58 -1
  174. package/src/plugins/bundle.ts +4 -2
  175. package/src/plugins/catalog-backend.test.ts +26 -0
  176. package/src/plugins/catalog-backend.ts +9 -2
  177. package/src/plugins/catalog-browser.test.ts +0 -13
  178. package/src/plugins/catalog-browser.ts +4 -0
  179. package/src/plugins/catalog-manifest.test.ts +15 -0
  180. package/src/plugins/catalog-manifest.ts +12 -5
  181. package/src/plugins/catalog-ui.ts +0 -6
  182. package/src/plugins/host-contract.ts +9 -0
  183. package/src/plugins/host-modules.ts +12 -0
  184. package/src/plugins/loader.ts +63 -15
  185. package/src/plugins/prediction-markets/headless.test.ts +119 -0
  186. package/src/plugins/prediction-markets/headless.ts +255 -0
  187. package/src/plugins/prediction-markets/index.tsx +2 -0
  188. package/src/plugins/seed.ts +3 -0
  189. package/src/public/broker.ts +6 -0
  190. package/src/remote/schema.test.ts +65 -0
  191. package/src/remote/schema.ts +322 -39
  192. package/src/remote/types.ts +49 -2
  193. package/src/renderers/browser/app-services.ts +3 -0
  194. package/src/renderers/browser/config-host.ts +24 -8
  195. package/src/renderers/browser/deeplink-bridge.test.ts +11 -0
  196. package/src/renderers/browser/deeplink-bridge.ts +15 -3
  197. package/src/renderers/browser/main.tsx +3 -1
  198. package/src/renderers/browser/research-entry.test.ts +8 -0
  199. package/src/renderers/browser/research-entry.ts +7 -0
  200. package/src/renderers/electrobun/bun/desktop/backend-requests.ts +2 -2
  201. package/src/renderers/electrobun/bun/desktop/initialization.ts +10 -3
  202. package/src/renderers/electrobun/bun/external-plugins.ts +20 -4
  203. package/src/renderers/electrobun/view/build-assets.ts +0 -2
  204. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +63 -10
  205. package/src/sources/gloomberb-cloud/news.ts +1 -0
  206. package/src/sync/core-contributors.test.ts +111 -0
  207. package/src/sync/core-contributors.ts +155 -1
  208. package/src/time-series/resolve.test.ts +118 -1
  209. package/src/time-series/resolve.ts +14 -16
  210. package/src/types/config.ts +2 -0
  211. package/src/plugins/broker-sync/broker-sync.test.ts +0 -188
  212. package/src/plugins/broker-sync/native-loader.ts +0 -10
  213. package/src/plugins/broker-sync/normalize.ts +0 -308
  214. package/src/plugins/broker-sync/public.ts +0 -122
  215. package/src/plugins/broker-sync/robinhood-native.ts +0 -367
  216. package/src/plugins/broker-sync/robinhood.ts +0 -114
  217. package/src/plugins/broker-sync/simplefin-native.ts +0 -201
  218. package/src/plugins/broker-sync/simplefin.ts +0 -70
  219. package/src/renderers/electrobun/view/native-stubs/broker-sync-native-loader.ts +0 -7
@@ -0,0 +1,153 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import type { PaneFunctionCatalog } from "../../../../cli/pane-functions/catalog";
3
+ import type {
4
+ HeadlessPaneArgumentDef,
5
+ HeadlessPaneDefinition,
6
+ PaneDef,
7
+ PaneTemplateDef,
8
+ } from "../../../../types/plugin";
9
+ import {
10
+ buildASKGToolManifests,
11
+ hashASKGToolManifests,
12
+ } from "./manifest";
13
+ import type { ClientToolManifest } from "./protocol";
14
+
15
+ function headless(argument: HeadlessPaneArgumentDef): HeadlessPaneDefinition<"rows"> {
16
+ return {
17
+ shape: "rows",
18
+ argument,
19
+ options: [{
20
+ key: "limit",
21
+ description: "Maximum rows.",
22
+ type: "integer",
23
+ minimum: 1,
24
+ maximum: 100,
25
+ defaultValue: 10,
26
+ settingKey: "limit",
27
+ pluginState: { pluginId: "test" },
28
+ }],
29
+ columns: [{ key: "value", header: "Value", format: String }],
30
+ load: () => ({ rows: [] }),
31
+ };
32
+ }
33
+
34
+ function pane(id: string): PaneDef {
35
+ return {
36
+ id,
37
+ name: id,
38
+ component: () => null,
39
+ defaultPosition: "left",
40
+ };
41
+ }
42
+
43
+ function template(
44
+ id: string,
45
+ paneId: string,
46
+ prefix: string,
47
+ argument: HeadlessPaneArgumentDef,
48
+ ): PaneTemplateDef {
49
+ return {
50
+ id,
51
+ paneId,
52
+ label: id,
53
+ description: `Read ${id}.`,
54
+ shortcut: { prefix, argKind: "text" },
55
+ headless: headless(argument),
56
+ };
57
+ }
58
+
59
+ function registry(templates: PaneTemplateDef[]): PaneFunctionCatalog {
60
+ return {
61
+ panes: new Map(templates.map(({ paneId }) => [paneId, pane(paneId)])),
62
+ paneTemplates: new Map(templates.map((entry) => [entry.id, entry])),
63
+ destroy() {},
64
+ };
65
+ }
66
+
67
+ function headlessTools(catalog: PaneFunctionCatalog): ClientToolManifest[] {
68
+ return buildASKGToolManifests(catalog).tools.filter(({ source }) => source === "headless");
69
+ }
70
+
71
+ describe("ASKG client manifest", () => {
72
+ test("projects every headless argument kind and strips renderer-only fields", () => {
73
+ const tools = headlessTools(registry([
74
+ template("none", "pane-none", "NON", { kind: "none" }),
75
+ template("ticker", "pane-ticker", "TIK", { kind: "ticker", placeholder: "symbol" }),
76
+ template("tickers", "pane-tickers", "TKS", { kind: "tickers", minimum: 2, maximum: 4 }),
77
+ template("symbols", "pane-symbols", "SYM", { kind: "symbol-list", optional: true }),
78
+ template("text", "pane-text", "TXT", { kind: "free-text", description: "Search phrase." }),
79
+ ]));
80
+
81
+ expect(tools.map(({ name, argument }) => [name, argument?.kind])).toEqual([
82
+ ["non", "none"],
83
+ ["sym", "symbol-list"],
84
+ ["tik", "ticker"],
85
+ ["tks", "tickers"],
86
+ ["txt", "free-text"],
87
+ ]);
88
+ expect(tools[0]?.options?.[0]).toEqual({
89
+ key: "limit",
90
+ description: "Maximum rows.",
91
+ type: "integer",
92
+ minimum: 1,
93
+ maximum: 100,
94
+ defaultValue: 10,
95
+ });
96
+ expect(tools[0]?.columns).toEqual([{ key: "value", header: "Value" }]);
97
+ expect(tools.every(({ writeTier, confirm }) => writeTier === "read" && confirm === "never")).toBe(true);
98
+ });
99
+
100
+ test("projects remote operation schemas and confirmation tiers", () => {
101
+ const { tools } = buildASKGToolManifests(registry([]));
102
+ const notify = tools.find(({ name }) => name === "app.notify");
103
+ const deleteLayout = tools.find(({ name }) => name === "layout.delete");
104
+
105
+ expect(notify).toMatchObject({
106
+ source: "remote-op",
107
+ writeTier: "ui-write",
108
+ confirm: "never",
109
+ inputSchema: { required: ["body"] },
110
+ });
111
+ expect(deleteLayout).toMatchObject({
112
+ source: "remote-op",
113
+ writeTier: "user-data",
114
+ confirm: "always",
115
+ inputSchema: { required: ["index"] },
116
+ });
117
+ });
118
+
119
+ test("hash is order-independent and changes with the surface", async () => {
120
+ const tools = headlessTools(registry([
121
+ template("valuation", "pane-valuation", "VAL", { kind: "none" }),
122
+ template("fear", "pane-fear", "FNG", { kind: "none" }),
123
+ ]));
124
+ const reversed = [...tools].reverse();
125
+ const changed = tools.map((tool, index) => index === 0
126
+ ? { ...tool, description: `${tool.description} Changed.` }
127
+ : tool);
128
+
129
+ expect(await hashASKGToolManifests(tools)).toBe(await hashASKGToolManifests(reversed));
130
+ expect(await hashASKGToolManifests(changed)).not.toBe(await hashASKGToolManifests(tools));
131
+ });
132
+
133
+ test("skips illegal and ambiguous names instead of rewriting them", () => {
134
+ const catalog = registry([
135
+ // Short and digit leading tokens are legal: the terminal's own shortcuts
136
+ // include N, SI and 13F, and a tool the model cannot name is a tool the
137
+ // model cannot use.
138
+ template("short", "pane-short", "N", { kind: "none" }),
139
+ template("numeric", "pane-numeric", "13F", { kind: "none" }),
140
+ template("bad", "pane-bad", "BAD/TOKEN", { kind: "none" }),
141
+ template("first", "pane-first", "VAL", { kind: "none" }),
142
+ template("second", "pane-second", "val", { kind: "none" }),
143
+ ]);
144
+ const { tools, skipped } = buildASKGToolManifests(catalog);
145
+
146
+ expect(tools.filter(({ source }) => source === "headless").map(({ name }) => name))
147
+ .toEqual(["13f", "n"]);
148
+ expect(skipped.map(({ token }) => token)).toEqual(["BAD/TOKEN", "VAL", "val"]);
149
+ expect(skipped.filter(({ token }) => token.toLowerCase() === "val").every(({ reason }) => (
150
+ reason.includes("Duplicate tool name")
151
+ ))).toBe(true);
152
+ });
153
+ });
@@ -0,0 +1,314 @@
1
+ import type { PaneFunctionCatalog } from "../../../../cli/pane-functions/catalog";
2
+ import { stableStringify } from "../../../../remote/revision";
3
+ import {
4
+ REMOTE_OPERATIONS,
5
+ REMOTE_RESOURCES,
6
+ remoteOperationDescriptors,
7
+ remoteOperationToolName,
8
+ } from "../../../../remote/schema";
9
+ import type { RemoteControlRequest } from "../../../../remote/types";
10
+ import type {
11
+ HeadlessPaneDefinition,
12
+ PaneDef,
13
+ PaneTemplateDef,
14
+ } from "../../../../types/plugin";
15
+ import {
16
+ TOOL_NAME_PATTERN,
17
+ type ClientToolManifest,
18
+ type ClientToolManifestSource,
19
+ type JsonValue,
20
+ type ToolManifestArgument,
21
+ type ToolManifestColumn,
22
+ type ToolManifestOption,
23
+ } from "./protocol";
24
+
25
+ export const HEADLESS_TOOL_TIMEOUT_MS = 30_000;
26
+ export const REMOTE_TOOL_TIMEOUT_MS = 10_000;
27
+ export const REMOTE_RESOURCE_TOOL_NAME = "app.get_resource";
28
+
29
+ const TOOL_NAME_REGEX = new RegExp(TOOL_NAME_PATTERN);
30
+
31
+ function compareText(left: string, right: string): number {
32
+ return left < right ? -1 : left > right ? 1 : 0;
33
+ }
34
+
35
+ export interface SkippedToolManifest {
36
+ source: ClientToolManifestSource;
37
+ token: string;
38
+ reason: string;
39
+ }
40
+
41
+ export interface ASKGClientManifest {
42
+ tools: ClientToolManifest[];
43
+ manifestHash: string;
44
+ skipped: SkippedToolManifest[];
45
+ }
46
+
47
+ interface ManifestCandidate {
48
+ token: string;
49
+ identity: string;
50
+ manifest: ClientToolManifest;
51
+ }
52
+
53
+ export type RemoteToolBinding =
54
+ | { kind: "operation"; operation: string }
55
+ | { kind: "resource" };
56
+
57
+ function projectArgument(argument: HeadlessPaneDefinition["argument"]): ToolManifestArgument {
58
+ return { ...argument };
59
+ }
60
+
61
+ function projectOptions(options: HeadlessPaneDefinition["options"]): ToolManifestOption[] {
62
+ return options.map(({
63
+ settingKey: _settingKey,
64
+ pluginState: _pluginState,
65
+ values,
66
+ aliases,
67
+ ...option
68
+ }) => ({
69
+ ...option,
70
+ ...(aliases ? { aliases: [...aliases] } : {}),
71
+ ...(values ? {
72
+ values: values.map((value) => ({
73
+ ...value,
74
+ ...(value.aliases ? { aliases: [...value.aliases] } : {}),
75
+ })),
76
+ } : {}),
77
+ }));
78
+ }
79
+
80
+ function projectColumns(columns: HeadlessPaneDefinition["columns"]): ToolManifestColumn[] | undefined {
81
+ if (!columns) return undefined;
82
+ return columns.map(({ format: _format, ...column }) => ({ ...column }));
83
+ }
84
+
85
+ function headlessManifest(
86
+ token: string,
87
+ title: string,
88
+ description: string,
89
+ definition: HeadlessPaneDefinition,
90
+ ): ClientToolManifest {
91
+ const columns = projectColumns(definition.columns);
92
+ return {
93
+ name: token.toLowerCase(),
94
+ source: "headless",
95
+ title,
96
+ description,
97
+ writeTier: "read",
98
+ shape: definition.shape,
99
+ argument: projectArgument(definition.argument),
100
+ options: projectOptions(definition.options),
101
+ ...(columns ? { columns } : {}),
102
+ confirm: "never",
103
+ timeoutMs: HEADLESS_TOOL_TIMEOUT_MS,
104
+ };
105
+ }
106
+
107
+ function definitionFor(template: PaneTemplateDef, pane: PaneDef): HeadlessPaneDefinition | undefined {
108
+ return template.headless ?? pane.headless;
109
+ }
110
+
111
+ function headlessCandidates(registry: PaneFunctionCatalog): ManifestCandidate[] {
112
+ const candidates: ManifestCandidate[] = [];
113
+ const templatedPaneIds = new Set<string>();
114
+
115
+ const templates = [...registry.paneTemplates.values()]
116
+ .sort((left, right) => compareText(left.id, right.id));
117
+ for (const template of templates) {
118
+ const pane = registry.panes.get(template.paneId);
119
+ if (!pane) continue;
120
+ templatedPaneIds.add(pane.id);
121
+ const definition = definitionFor(template, pane);
122
+ if (!definition) continue;
123
+ const token = template.shortcut?.prefix ?? template.id;
124
+ candidates.push({
125
+ token,
126
+ identity: `template:${template.id}`,
127
+ manifest: headlessManifest(token, template.label, template.description, definition),
128
+ });
129
+ }
130
+
131
+ const panes = [...registry.panes.values()]
132
+ .sort((left, right) => compareText(left.id, right.id));
133
+ for (const pane of panes) {
134
+ if (templatedPaneIds.has(pane.id) || !pane.headless) continue;
135
+ candidates.push({
136
+ token: pane.id,
137
+ identity: `pane:${pane.id}`,
138
+ manifest: headlessManifest(
139
+ pane.id,
140
+ pane.name,
141
+ `Read data from the ${pane.name} pane.`,
142
+ pane.headless,
143
+ ),
144
+ });
145
+ }
146
+
147
+ return candidates;
148
+ }
149
+
150
+ function remoteCandidates(): ManifestCandidate[] {
151
+ const descriptors = remoteOperationDescriptors();
152
+ const operationsByName = new Map(
153
+ REMOTE_OPERATIONS.map((operation) => [remoteOperationToolName(operation.id), operation]),
154
+ );
155
+ const candidates = descriptors.map((descriptor): ManifestCandidate => ({
156
+ token: descriptor.name,
157
+ identity: `operation:${operationsByName.get(descriptor.name)?.id ?? descriptor.name}`,
158
+ manifest: {
159
+ name: descriptor.name,
160
+ source: "remote-op",
161
+ title: descriptor.title,
162
+ description: descriptor.description,
163
+ writeTier: descriptor.writeTier,
164
+ inputSchema: descriptor.inputSchema,
165
+ confirm: descriptor.writeTier === "user-data" || descriptor.writeTier === "broker"
166
+ ? "always"
167
+ : "never",
168
+ timeoutMs: REMOTE_TOOL_TIMEOUT_MS,
169
+ },
170
+ }));
171
+
172
+ candidates.push({
173
+ token: REMOTE_RESOURCE_TOOL_NAME,
174
+ identity: "resource:get",
175
+ manifest: {
176
+ name: REMOTE_RESOURCE_TOOL_NAME,
177
+ source: "remote-op",
178
+ title: "App: Get resource",
179
+ description: `Read a registered in-process app resource. Available resources: ${REMOTE_RESOURCES.map(({ uri }) => uri).join(", ")}.`,
180
+ writeTier: "read",
181
+ inputSchema: {
182
+ type: "object",
183
+ properties: {
184
+ resource: {
185
+ type: "string",
186
+ minLength: 1,
187
+ description: "Registered resource URI, such as app://snapshot or app://pane-state/{paneId}.",
188
+ },
189
+ },
190
+ required: ["resource"],
191
+ additionalProperties: false,
192
+ },
193
+ confirm: "never",
194
+ timeoutMs: REMOTE_TOOL_TIMEOUT_MS,
195
+ },
196
+ });
197
+
198
+ return candidates;
199
+ }
200
+
201
+ function compareManifests(left: ClientToolManifest, right: ClientToolManifest): number {
202
+ return compareText(left.name, right.name) || compareText(left.source, right.source);
203
+ }
204
+
205
+ function filterCandidates(candidates: ManifestCandidate[]): {
206
+ tools: ClientToolManifest[];
207
+ skipped: SkippedToolManifest[];
208
+ } {
209
+ const skipped: SkippedToolManifest[] = [];
210
+ const valid: ManifestCandidate[] = [];
211
+
212
+ for (const candidate of candidates.sort((left, right) => (
213
+ compareText(left.manifest.name, right.manifest.name)
214
+ || compareText(left.identity, right.identity)
215
+ ))) {
216
+ if (!TOOL_NAME_REGEX.test(candidate.manifest.name)) {
217
+ skipped.push({
218
+ source: candidate.manifest.source,
219
+ token: candidate.token,
220
+ reason: `Lowercased token "${candidate.manifest.name}" does not match ${TOOL_NAME_PATTERN}.`,
221
+ });
222
+ continue;
223
+ }
224
+ valid.push(candidate);
225
+ }
226
+
227
+ const counts = new Map<string, number>();
228
+ for (const candidate of valid) {
229
+ counts.set(candidate.manifest.name, (counts.get(candidate.manifest.name) ?? 0) + 1);
230
+ }
231
+
232
+ const tools: ClientToolManifest[] = [];
233
+ for (const candidate of valid) {
234
+ if ((counts.get(candidate.manifest.name) ?? 0) > 1) {
235
+ skipped.push({
236
+ source: candidate.manifest.source,
237
+ token: candidate.token,
238
+ reason: `Duplicate tool name "${candidate.manifest.name}" from ${candidate.identity}.`,
239
+ });
240
+ continue;
241
+ }
242
+ tools.push(candidate.manifest);
243
+ }
244
+
245
+ return {
246
+ tools: tools.sort(compareManifests),
247
+ skipped: skipped.sort((left, right) => (
248
+ compareText(left.token, right.token) || compareText(left.source, right.source)
249
+ )),
250
+ };
251
+ }
252
+
253
+ export function buildASKGToolManifests(registry: PaneFunctionCatalog): {
254
+ tools: ClientToolManifest[];
255
+ skipped: SkippedToolManifest[];
256
+ } {
257
+ return filterCandidates([
258
+ ...headlessCandidates(registry),
259
+ ...remoteCandidates(),
260
+ ]);
261
+ }
262
+
263
+ function bytesToHex(bytes: Uint8Array): string {
264
+ return [...bytes].map((byte) => byte.toString(16).padStart(2, "0")).join("");
265
+ }
266
+
267
+ export async function hashASKGToolManifests(tools: readonly ClientToolManifest[]): Promise<string> {
268
+ const sorted = [...tools].sort(compareManifests);
269
+ const digest = await globalThis.crypto.subtle.digest(
270
+ "SHA-256",
271
+ new TextEncoder().encode(stableStringify(sorted)),
272
+ );
273
+ return `sha256:${bytesToHex(new Uint8Array(digest))}`;
274
+ }
275
+
276
+ export async function buildASKGClientManifest(
277
+ registry: PaneFunctionCatalog,
278
+ ): Promise<ASKGClientManifest> {
279
+ const { tools, skipped } = buildASKGToolManifests(registry);
280
+ return {
281
+ tools,
282
+ manifestHash: await hashASKGToolManifests(tools),
283
+ skipped,
284
+ };
285
+ }
286
+
287
+ export function resolveRemoteToolBinding(name: string): RemoteToolBinding | null {
288
+ if (name === REMOTE_RESOURCE_TOOL_NAME) return { kind: "resource" };
289
+ const operation = REMOTE_OPERATIONS.find((entry) => remoteOperationToolName(entry.id) === name);
290
+ return operation ? { kind: "operation", operation: operation.id } : null;
291
+ }
292
+
293
+ function resourcePatternMatches(pattern: string, value: string): boolean {
294
+ const escaped = pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
295
+ const regex = new RegExp(`^${escaped.replace(/\\\{[^}]+\\\}/g, "[^/]+")}$`);
296
+ return regex.test(value);
297
+ }
298
+
299
+ export function remoteRequestForTool(
300
+ binding: RemoteToolBinding,
301
+ args: Record<string, JsonValue>,
302
+ ): RemoteControlRequest {
303
+ if (binding.kind === "operation") {
304
+ return { type: "call", operation: binding.operation, input: args };
305
+ }
306
+ const resource = args.resource;
307
+ if (typeof resource !== "string" || resource.length === 0) {
308
+ throw new Error(`${REMOTE_RESOURCE_TOOL_NAME} requires a resource URI.`);
309
+ }
310
+ if (!REMOTE_RESOURCES.some(({ uri }) => resourcePatternMatches(uri, resource))) {
311
+ throw new Error(`Unknown remote resource "${resource}".`);
312
+ }
313
+ return { type: "get", resource };
314
+ }
@@ -0,0 +1,190 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import type { HeadlessPaneDefinition } from "../../../../types/headless";
3
+ import { marketValuationHeadless } from "../../market-valuation/headless";
4
+ import {
5
+ ASKG_PROTOCOL_VERSION,
6
+ TOOL_NAME_PATTERN,
7
+ type ASKGSessionStartRequest,
8
+ type ASKGSessionStartResponse,
9
+ type ASKGSseEvent,
10
+ type ClientToolManifest,
11
+ type ToolManifest,
12
+ type ToolResultPayload,
13
+ } from "./protocol";
14
+
15
+ function jsonRoundTrip<T>(value: T): T {
16
+ return JSON.parse(JSON.stringify(value)) as T;
17
+ }
18
+
19
+ function projectHeadlessDefinition(
20
+ definition: HeadlessPaneDefinition,
21
+ ): ClientToolManifest {
22
+ const options = definition.options.map(({ settingKey: _settingKey, pluginState: _pluginState, ...option }) => option);
23
+ const columns = definition.columns?.map(({ format: _format, ...column }) => column);
24
+ return {
25
+ name: "market.valuation",
26
+ source: "headless",
27
+ title: "Market valuation",
28
+ description: "Compare whole-market valuation ratios with their history.",
29
+ writeTier: "read",
30
+ shape: definition.shape,
31
+ argument: definition.argument,
32
+ options,
33
+ ...(columns ? { columns } : {}),
34
+ confirm: "never",
35
+ timeoutMs: 30_000,
36
+ };
37
+ }
38
+
39
+ describe("ASKG protocol", () => {
40
+ test("round-trips session negotiation, a manifest, every event, and a tool result", () => {
41
+ const manifest: ClientToolManifest = {
42
+ name: "layout.place_pane",
43
+ source: "remote-op",
44
+ title: "Place pane",
45
+ description: "Move a pane to a layout region.",
46
+ writeTier: "ui-write",
47
+ inputSchema: {
48
+ type: "object",
49
+ properties: {
50
+ paneId: { type: "string" },
51
+ region: { type: "string", enum: ["left", "right", "floating"] },
52
+ },
53
+ required: ["paneId", "region"],
54
+ additionalProperties: false,
55
+ },
56
+ confirm: "never",
57
+ timeoutMs: 10_000,
58
+ };
59
+ const sessionRequest: ASKGSessionStartRequest = {
60
+ protocolVersion: ASKG_PROTOCOL_VERSION,
61
+ client: { kind: "tui", version: "0.8.0" },
62
+ context: {
63
+ query: "Compare NVDA valuation",
64
+ symbol: "NVDA",
65
+ paneId: "ticker:main",
66
+ layout: { id: "layout-1" },
67
+ },
68
+ tools: [manifest],
69
+ manifestHash: "sha256:manifest-1",
70
+ };
71
+ const sessionResponse: ASKGSessionStartResponse = {
72
+ protocolVersion: ASKG_PROTOCOL_VERSION,
73
+ sessionId: "session-1",
74
+ manifestHash: sessionRequest.manifestHash,
75
+ serverTools: [],
76
+ acceptedTools: [manifest.name],
77
+ rejectedTools: [{ name: "invalid.tool", reason: "Unknown source" }],
78
+ limits: {
79
+ requestsPerMinute: 20,
80
+ turnsPerDay: 300,
81
+ turnsRemainingToday: 299,
82
+ maxToolCallsPerTurn: 25,
83
+ turnWallClockMs: 90_000,
84
+ clientToolTimeoutMs: 10_000,
85
+ },
86
+ tier: "pro",
87
+ model: "gpt-5.6-luna",
88
+ promptVersion: "2026-09-04",
89
+ expiresAt: "2026-09-04T21:30:00.000Z",
90
+ };
91
+ const events: ASKGSseEvent[] = [
92
+ {
93
+ type: "session",
94
+ seq: 1,
95
+ sessionId: "session-1",
96
+ turnId: "turn-1",
97
+ model: "gpt-5.6-luna",
98
+ promptVersion: "2026-09-04",
99
+ },
100
+ { type: "text-delta", seq: 2, turnId: "turn-1", delta: "NVDA" },
101
+ {
102
+ type: "tool-call",
103
+ seq: 3,
104
+ turnId: "turn-1",
105
+ toolCallId: "call-1",
106
+ name: manifest.name,
107
+ args: { paneId: "chart:main", region: "right" },
108
+ writeTier: "ui-write",
109
+ requiresConfirmation: false,
110
+ preview: { paneId: "chart:main", region: "right", dryRun: true },
111
+ timeoutMs: 10_000,
112
+ expiresAt: "2026-09-04T21:00:10.000Z",
113
+ },
114
+ {
115
+ type: "tool-executed",
116
+ seq: 4,
117
+ turnId: "turn-1",
118
+ toolCallId: "call-server-1",
119
+ name: "market.quote",
120
+ source: "server",
121
+ status: "ok",
122
+ summary: {
123
+ rowCount: 1,
124
+ elapsedMs: 24,
125
+ truncated: false,
126
+ note: "Fresh quote",
127
+ sample: { symbol: "NVDA", price: 180 },
128
+ },
129
+ },
130
+ { type: "tool-result-ack", seq: 5, turnId: "turn-1", toolCallId: "call-1" },
131
+ {
132
+ type: "error",
133
+ seq: 6,
134
+ turnId: "turn-1",
135
+ code: "rate_limited",
136
+ message: "Try again shortly.",
137
+ retryable: true,
138
+ retryAfterMs: 1_000,
139
+ },
140
+ {
141
+ type: "usage",
142
+ seq: 7,
143
+ turnId: "turn-1",
144
+ inputTokens: 120,
145
+ outputTokens: 30,
146
+ totalTokens: 150,
147
+ estimated: false,
148
+ },
149
+ { type: "done", seq: 8, turnId: "turn-1", reason: "complete" },
150
+ ];
151
+ const result: ToolResultPayload = {
152
+ turnId: "turn-1",
153
+ toolCallId: "call-1",
154
+ status: "ok",
155
+ result: { moved: true },
156
+ rowCount: 1,
157
+ truncated: false,
158
+ elapsedMs: 12,
159
+ rev: "rev-2",
160
+ undoToken: "undo-1",
161
+ };
162
+
163
+ expect(new RegExp(TOOL_NAME_PATTERN).test(manifest.name)).toBe(true);
164
+ expect(jsonRoundTrip(manifest)).toEqual(manifest);
165
+ expect(jsonRoundTrip(sessionRequest)).toEqual(sessionRequest);
166
+ expect(jsonRoundTrip(sessionResponse)).toEqual(sessionResponse);
167
+ expect(events.map(jsonRoundTrip)).toEqual(events);
168
+ expect(jsonRoundTrip(result)).toEqual(result);
169
+ });
170
+
171
+ test("projects a migrated headless definition without executable fields", () => {
172
+ const manifest = projectHeadlessDefinition(marketValuationHeadless);
173
+ const serialized = JSON.stringify(manifest);
174
+
175
+ expect(new RegExp(TOOL_NAME_PATTERN).test(manifest.name)).toBe(true);
176
+ expect(manifest).toMatchObject({
177
+ source: "headless",
178
+ writeTier: "read",
179
+ shape: marketValuationHeadless.shape,
180
+ argument: marketValuationHeadless.argument,
181
+ confirm: "never",
182
+ });
183
+ expect(manifest.options).toHaveLength(marketValuationHeadless.options.length);
184
+ expect(serialized).not.toContain("load");
185
+ expect(serialized).not.toContain("settingKey");
186
+ expect(serialized).not.toContain("pluginState");
187
+ expect(serialized).not.toContain("format");
188
+ expect(jsonRoundTrip(manifest)).toEqual(manifest);
189
+ });
190
+ });