@apiosk/mcp 1.3.1 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/README.md +84 -537
  2. package/assets/brand/apiosk-a-20260921.png +0 -0
  3. package/assets/brand/apple-touch-icon.png +0 -0
  4. package/assets/brand/favicon-dark.ico +0 -0
  5. package/assets/brand/favicon-light.ico +0 -0
  6. package/assets/brand/favicon.ico +0 -0
  7. package/assets/brand/icon-192.png +0 -0
  8. package/assets/brand/icon-512.png +0 -0
  9. package/assets/brand/icon-maskable-512.png +0 -0
  10. package/assets/brand/inter-OFL.txt +93 -0
  11. package/assets/brand/inter-latin-400-normal.woff2 +0 -0
  12. package/assets/brand/inter-latin-500-normal.woff2 +0 -0
  13. package/assets/brand/inter-latin-600-normal.woff2 +0 -0
  14. package/assets/brand/mark-20260905-transparent.svg +1 -0
  15. package/assets/brand/mark-20260918.svg +1 -0
  16. package/assets/brand/mark-black-20260918.svg +1 -0
  17. package/assets/brand/mark-dark-20260905-transparent.png +0 -0
  18. package/assets/brand/mark-dark-20260918.png +0 -0
  19. package/assets/brand/mark-dark-96.png +0 -0
  20. package/assets/brand/mark-light-20260905-transparent.png +0 -0
  21. package/assets/brand/mark-light-20260918.png +0 -0
  22. package/assets/brand/mark-light-96.png +0 -0
  23. package/assets/brand/wordmark-black-320.png +0 -0
  24. package/assets/brand/wordmark-white-320.png +0 -0
  25. package/docs/branding.md +11 -0
  26. package/docs/marketplace-submission-2026-04-08.md +4 -0
  27. package/docs/marketplace-submission-2026-08-20.md +157 -0
  28. package/docs/openai-plugin-submission-2026-09-05.md +143 -0
  29. package/docs/sepa-rail.md +13 -13
  30. package/dxt.json +26 -22
  31. package/index.mjs +3 -1
  32. package/logo-optimized-light.png +0 -0
  33. package/package.json +15 -13
  34. package/plugin/apiosk/.codex-plugin/plugin.json +45 -0
  35. package/plugin/apiosk/.mcp.json +8 -0
  36. package/plugin/apiosk/assets/icon-dark.png +0 -0
  37. package/plugin/apiosk/assets/icon.png +0 -0
  38. package/plugin/apiosk/assets/icon.svg +1 -0
  39. package/plugin/apiosk/assets/logo.png +0 -0
  40. package/plugin/apiosk/skills/apiosk/SKILL.md +33 -0
  41. package/plugin/apiosk/skills/apiosk/agents/openai.yaml +12 -0
  42. package/server.json +98 -9
  43. package/server.mjs +258 -71
  44. package/src/approval-feedback.mjs +21 -0
  45. package/src/brand-routes.mjs +28 -0
  46. package/src/create-server.mjs +168 -9
  47. package/src/display-money.mjs +20 -0
  48. package/src/display-text.mjs +67 -0
  49. package/src/gateway-client.mjs +42 -0
  50. package/src/gateway-v2-ask.mjs +50 -0
  51. package/src/gateway-v2-card-account.mjs +23 -0
  52. package/src/gateway-v2-card-actions.mjs +67 -0
  53. package/src/gateway-v2-card-answer-text.mjs +130 -0
  54. package/src/gateway-v2-card-answer.mjs +68 -0
  55. package/src/gateway-v2-card-blocks.mjs +166 -0
  56. package/src/gateway-v2-card-body.mjs +259 -0
  57. package/src/gateway-v2-card-budget.mjs +21 -0
  58. package/src/gateway-v2-card-cbs.mjs +56 -0
  59. package/src/gateway-v2-card-choices.mjs +4 -0
  60. package/src/gateway-v2-card-clarification.mjs +25 -0
  61. package/src/gateway-v2-card-compact.mjs +34 -0
  62. package/src/gateway-v2-card-events.mjs +41 -0
  63. package/src/gateway-v2-card-presentation.mjs +125 -0
  64. package/src/gateway-v2-card-research.mjs +63 -0
  65. package/src/gateway-v2-card-result.mjs +32 -0
  66. package/src/gateway-v2-card-search.mjs +39 -0
  67. package/src/gateway-v2-card-sources.mjs +32 -0
  68. package/src/gateway-v2-card-style.mjs +76 -0
  69. package/src/gateway-v2-card-verdict.mjs +66 -0
  70. package/src/gateway-v2-card.mjs +91 -0
  71. package/src/gateway-v2-contracts.json +66 -0
  72. package/src/gateway-v2-instructions.md +113 -0
  73. package/src/gateway-v2-recovery.mjs +18 -0
  74. package/src/gateway-v2-report-links.mjs +14 -0
  75. package/src/gateway-v2-workflows.mjs +17 -0
  76. package/src/gateway-v2.mjs +179 -0
  77. package/src/oauth.mjs +603 -364
  78. package/src/observability.mjs +210 -0
  79. package/src/result-presentation.mjs +9 -0
  80. package/src/runtime.mjs +24 -3091
  81. package/src/settlement-disclosure.mjs +26 -0
  82. package/src/source-groups.mjs +91 -0
  83. package/src/source-value-format.mjs +25 -0
  84. package/src/tool-result.mjs +20 -0
  85. package/src/ui-bridge.mjs +185 -0
  86. package/src/well-known-routes.mjs +132 -0
  87. package/src/funding-options.mjs +0 -255
  88. package/src/gateway-management.mjs +0 -107
  89. package/src/listing-metadata.mjs +0 -287
  90. package/src/local-config.mjs +0 -256
  91. package/src/payment-guidance.mjs +0 -307
  92. package/src/wallet-store.mjs +0 -476
@@ -2,36 +2,195 @@ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
2
2
  import {
3
3
  CallToolRequestSchema,
4
4
  ListToolsRequestSchema,
5
+ ListResourcesRequestSchema,
6
+ ReadResourceRequestSchema,
7
+ ListPromptsRequestSchema,
8
+ GetPromptRequestSchema,
5
9
  } from "@modelcontextprotocol/sdk/types.js";
6
- import { createApioskMcpRuntime } from "./runtime.mjs";
10
+ import { createApioskMcpRuntime, resolveGatewayV2Url } from "./runtime.mjs";
11
+ import { V2_INSTRUCTIONS, V2_DESCRIPTION, V2_RESOURCE } from "./gateway-v2.mjs";
12
+ import { APIO_V2_CARD_URI, APIO_V2_CHATGPT_CARD_URI, APIO_V2_CARD_LEGACY_URIS, APIO_V2_MODERN_CARD_URIS, gatewayV2CardHtml, gatewayV2CardMeta } from "./gateway-v2-card.mjs";
13
+
14
+ /**
15
+ * One sentence, defined once.
16
+ *
17
+ * Registries take a server's description from wherever they can find it: the
18
+ * server card, `serverInfo`, or by scraping the HTML at the root. So every one
19
+ * of those surfaces reads this constant, and changing the pitch means changing
20
+ * it in src/gateway-v2.mjs.
21
+ */
22
+ export const SERVER_DESCRIPTION = V2_DESCRIPTION;
23
+
24
+ // Base version, kept in step with the published manifests (package.json etc.).
25
+ export const SERVER_BASE_VERSION = "2.0.0";
26
+
27
+ // The millisecond timestamp encoded in the first 10 chars of a ULID (Crockford
28
+ // base32). Fly's FLY_MACHINE_VERSION is a ULID that changes on every deploy, and
29
+ // its timestamp is monotonically increasing, which is exactly the "counter"
30
+ // property a version needs. Returns null for anything that is not a ULID.
31
+ function ulidTimestampMs(ulid) {
32
+ const B32 = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
33
+ const head = String(ulid || "").toUpperCase().slice(0, 10);
34
+ if (head.length < 10) return null;
35
+ let ms = 0;
36
+ for (const ch of head) {
37
+ const v = B32.indexOf(ch);
38
+ if (v < 0) return null;
39
+ ms = ms * 32 + v;
40
+ }
41
+ return ms;
42
+ }
43
+
44
+ // The version a client reads on `initialize`. It must move on every `fly deploy`
45
+ // so a client that caches tool definitions can tell it is looking at a new build
46
+ // after a redeploy or reconnect. Fly provides no plain release counter, but it
47
+ // does set FLY_MACHINE_VERSION (a ULID) which changes each deploy; its timestamp
48
+ // (in seconds) becomes the patch, so each deploy reads as a strictly newer
49
+ // semver (the patch is respected where build metadata after '+' would be
50
+ // ignored). Falls back to the base version locally; APIOSK_MCP_VERSION pins it.
51
+ export function resolveServerVersion(env = process.env) {
52
+ const explicit = typeof env.APIOSK_MCP_VERSION === "string" ? env.APIOSK_MCP_VERSION.trim() : "";
53
+ if (explicit) return explicit;
54
+ const [major = "2", minor = "0"] = SERVER_BASE_VERSION.split(".");
55
+ const ms = ulidTimestampMs(env.FLY_MACHINE_VERSION || env.FLY_IMAGE_REF?.split("deployment-")?.[1]);
56
+ return ms ? `${major}.${minor}.${Math.floor(ms / 1000)}` : SERVER_BASE_VERSION;
57
+ }
58
+
59
+ /** Transparent brand mark, shared by initialize and the published server card.
60
+ * SVG follows the host color scheme; PNG fallbacks explicitly name their theme. */
61
+ export const SERVER_ICONS = [
62
+ {
63
+ src: "https://mcp.apiosk.com/brand/apiosk-a-20260921.png",
64
+ mimeType: "image/png",
65
+ sizes: ["1254x1254"],
66
+ theme: "light",
67
+ },
68
+ {
69
+ "src": "https://mcp.apiosk.com/brand/mark-20260918.svg",
70
+ "mimeType": "image/svg+xml",
71
+ "sizes": [
72
+ "any"
73
+ ]
74
+ },
75
+ {
76
+ "src": "https://mcp.apiosk.com/brand/mark-light-20260918.png",
77
+ "mimeType": "image/png",
78
+ "sizes": [
79
+ "512x512"
80
+ ],
81
+ "theme": "light"
82
+ },
83
+ {
84
+ "src": "https://mcp.apiosk.com/brand/mark-dark-20260918.png",
85
+ "mimeType": "image/png",
86
+ "sizes": [
87
+ "512x512"
88
+ ],
89
+ "theme": "dark"
90
+ }
91
+ ];
7
92
 
8
93
  export const SERVER_INFO = {
9
94
  name: "apiosk-mcp",
10
- version: "1.3.1",
95
+ version: resolveServerVersion(),
96
+ /**
97
+ * The word a host puts after "from" on its consent card — "Claude wants to
98
+ * use Plan a data request from Apiosk". The tool titles carry the verb, so
99
+ * the server carries only the brand.
100
+ */
101
+ title: "Apiosk",
102
+ description: SERVER_DESCRIPTION,
103
+ websiteUrl: "https://apiosk.com",
104
+ icons: SERVER_ICONS,
11
105
  };
12
106
 
107
+ // Shown to every connecting MCP client/agent as server-level guidance: the
108
+ // Gateway v2 host contract, synced from gateway/contracts/host-instructions.md.
109
+ export const SERVER_INSTRUCTIONS = V2_INSTRUCTIONS;
110
+
13
111
  function resolveRuntime(options = {}) {
14
112
  return options.runtime || createApioskMcpRuntime(options);
15
113
  }
16
114
 
17
115
  export async function listApioskTools(options = {}) {
18
- return resolveRuntime(options).listTools(options.authInfo);
116
+ return resolveRuntime(options).listTools();
117
+ }
118
+
119
+ export function resolveServerPresentation(env = process.env) {
120
+ return {
121
+ info: { ...SERVER_INFO, version: resolveServerVersion(env) },
122
+ description: SERVER_DESCRIPTION,
123
+ instructions: SERVER_INSTRUCTIONS,
124
+ };
19
125
  }
20
126
 
21
127
  export function createApioskMcpServer(options = {}) {
22
128
  const runtime = resolveRuntime(options);
129
+ const env = options.env || process.env;
130
+ const gatewayUrl = resolveGatewayV2Url(env);
131
+ const cardHtml = gatewayV2CardHtml(gatewayUrl);
132
+ const cardMeta = gatewayV2CardMeta(gatewayUrl);
23
133
  const server = new Server(
24
- SERVER_INFO,
25
- { capabilities: { tools: {} } }
134
+ resolveServerPresentation(env).info,
135
+ // `prompts` is declared because it is implemented, as an empty list.
136
+ // Leaving it out made prompts/list answer -32601 Method not found, which a
137
+ // scanner reads as a broken server rather than a server without prompts.
138
+ {
139
+ capabilities: { tools: {}, resources: {}, prompts: {} },
140
+ instructions: SERVER_INSTRUCTIONS,
141
+ }
26
142
  );
27
143
 
28
- server.setRequestHandler(ListToolsRequestSchema, async (_request, extra) => ({
29
- tools: await runtime.listTools(extra.authInfo),
144
+ /**
145
+ * The card, and the one MIME question.
146
+ *
147
+ * MCP Apps (SEP-1865) reads `text/html;profile=mcp-app` and OpenAI's Apps SDK
148
+ * reads `text/html+skybridge`. The current card has a separate, stable URI
149
+ * for each. Only previously issued compatible URIs keep the old
150
+ * host-dependent label: modern ChatGPT also sends an OpenAI user agent, so
151
+ * the user agent is not a MIME signal for the new URIs.
152
+ */
153
+ const legacyMime = () => (options.legacyUiMime ? "text/html+skybridge" : "text/html;profile=mcp-app");
154
+ const cardMime = (uri) => uri.endsWith("-chatgpt.html")
155
+ ? "text/html+skybridge"
156
+ : APIO_V2_MODERN_CARD_URIS.includes(uri) ? "text/html;profile=mcp-app" : legacyMime();
157
+
158
+ server.setRequestHandler(ListResourcesRequestSchema, async () => ({
159
+ resources: [
160
+ V2_RESOURCE,
161
+ { uri: APIO_V2_CARD_URI, name: "Apiosk Gateway v2 interactive card", mimeType: "text/html;profile=mcp-app", _meta: cardMeta },
162
+ { uri: APIO_V2_CHATGPT_CARD_URI, name: "Apiosk card for ChatGPT", mimeType: "text/html+skybridge", _meta: cardMeta },
163
+ ...APIO_V2_CARD_LEGACY_URIS.map((uri) => ({
164
+ uri,
165
+ name: "Apiosk Gateway v2 interactive card (compatible)",
166
+ mimeType: cardMime(uri),
167
+ _meta: cardMeta,
168
+ })),
169
+ ],
30
170
  }));
31
171
 
32
- server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
33
- return runtime.callTool(request.params.name, request.params.arguments || {}, extra.authInfo);
172
+ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
173
+ const { uri } = request.params;
174
+ if (uri === V2_RESOURCE.uri) return { contents: [{ uri, mimeType: V2_RESOURCE.mimeType, text: V2_INSTRUCTIONS }] };
175
+ if (uri === APIO_V2_CHATGPT_CARD_URI || APIO_V2_MODERN_CARD_URIS.includes(uri) || APIO_V2_CARD_LEGACY_URIS.includes(uri)) {
176
+ return { contents: [{ uri, mimeType: cardMime(uri), text: cardHtml, _meta: cardMeta }] };
177
+ }
178
+ throw new Error("Unknown Apiosk resource");
179
+ });
180
+
181
+ server.setRequestHandler(ListPromptsRequestSchema, async () => ({ prompts: [] }));
182
+
183
+ server.setRequestHandler(GetPromptRequestSchema, async () => {
184
+ throw new Error("Use apiosk_discover with your question");
34
185
  });
35
186
 
187
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
188
+ tools: await runtime.listTools(),
189
+ }));
190
+
191
+ server.setRequestHandler(CallToolRequestSchema, async (request, extra) =>
192
+ runtime.callTool(request.params.name, request.params.arguments || {}, extra.authInfo)
193
+ );
194
+
36
195
  return server;
37
196
  }
@@ -0,0 +1,20 @@
1
+ // Self-contained: this exact function is also embedded in the chat card.
2
+ // Conversion affects display only, never the amount submitted for approval.
3
+ export function formatDisplayMoney(atomic, currency = 'USD', display = null, ceiling = false) {
4
+ if (atomic == null || !/^-?\d+$/.test(String(atomic))) return null;
5
+ currency = !currency || currency === 'USDC' ? 'USD' : currency;
6
+ let value = BigInt(atomic);
7
+ if (currency === 'USD' && display?.base_currency === 'USD' && /^[A-Z]{3}$/.test(display.currency || '') && /^\d{1,18}(\.\d{1,18})?$/.test(display.rate || '')) {
8
+ const [whole, fraction = ''] = display.rate.split('.');
9
+ const numerator = BigInt(whole + fraction), denominator = 10n ** BigInt(fraction.length);
10
+ if (numerator > 0n) {
11
+ const product = value * numerator;
12
+ const negative = product < 0n, magnitude = negative ? -product : product;
13
+ const rounding = ceiling && !negative ? denominator - 1n : denominator / 2n;
14
+ value = (negative ? -1n : 1n) * ((magnitude + rounding) / denominator);
15
+ currency = display.currency;
16
+ }
17
+ }
18
+ const negative = value < 0n, magnitude = negative ? -value : value;
19
+ return `${negative ? '-' : ''}${magnitude / 1000000n}.${(magnitude % 1000000n).toString().padStart(6, '0').replace(/0+$/, '').padEnd(2, '0')} ${currency}`;
20
+ }
@@ -0,0 +1,67 @@
1
+ // Keep in sync with App src/lib/ui/display-text.ts.
2
+ const displayAbbreviations = new Set([
3
+ 'BV', 'NV', 'VOF', 'CV', 'LLC', 'LLP', 'PLC', 'AG', 'SA', 'SAS', 'SE', 'ASA',
4
+ 'IT', 'ICT', 'AI', 'IBM', 'ING', 'ABN', 'AMRO', 'ASML', 'KPN', 'TNO',
5
+ 'EU', 'UK', 'US', 'USA', 'NL', 'EUR', 'USD', 'GBP', 'VAT', 'KVK', 'API', 'HTTP', 'HTTPS',
6
+ ]);
7
+ /** Technical values stay literal regardless of the response's source or shape. */
8
+ export function isLiteralField(key) {
9
+ const words = key.replace(/([a-z])([A-Z])/g, '$1 $2').split(/[^a-zA-Z]+/).filter(Boolean);
10
+ return words.some(word => /^(?:id|ids|code|codes|ref|refs|reference|identifier|identifiers|token|key|hash|signature|url|uri|path|pointer|email|domain|postcode|postal|zip|iban|bic|phone|number|nummer|currency|unit|symbol|method)$/i.test(word));
11
+ }
12
+ /** Shared display rule for human-readable labels and values, independent of answer type. */
13
+ export function formatDisplayText(text, key = '') {
14
+ if (isLiteralField(key) || text.length > 160 || text !== text.toLocaleUpperCase('nl-NL') || !/\p{L}/u.test(text))
15
+ return text;
16
+ if (/[@:/_\n]|\p{L}\d|\d\p{L}/u.test(text) || /^\S+\.[A-Z]{2,}$/u.test(text))
17
+ return text;
18
+ if (!/\s/u.test(text) && text.length <= 4)
19
+ return text;
20
+ return text.replace(/\p{L}[\p{L}\p{M}]*(?:['’][\p{L}\p{M}]+)*/gu, word => {
21
+ if (displayAbbreviations.has(word))
22
+ return word;
23
+ if (word === 'GMBH')
24
+ return 'GmbH';
25
+ return word.charAt(0) + word.slice(1).toLocaleLowerCase('nl-NL');
26
+ });
27
+ }
28
+ /** A display copy for any nested JSON response. Never modifies the source or its keys. */
29
+ export function displayData(value, key = '', depth = 0) {
30
+ if (depth > 20 || value == null || isLiteralField(key))
31
+ return value;
32
+ if (typeof value === 'string')
33
+ return formatDisplayText(value, key);
34
+ if (Array.isArray(value))
35
+ return value.map(child => displayData(child, key, depth + 1));
36
+ if (typeof value !== 'object')
37
+ return value;
38
+ const record = value;
39
+ const field = record.key ?? record.field ?? record.type ?? record.factType ?? record.pointer;
40
+ return Object.fromEntries(Object.entries(record).map(([name, child]) => [name,
41
+ displayData(child, name === 'value' && typeof field === 'string' ? field : name, depth + 1),
42
+ ]));
43
+ }
44
+ /** Use the same spelling in prose as in the displayed source values. */
45
+ export function formatDisplayNarrative(text, context) {
46
+ const replacements = new Map();
47
+ function collect(raw, shown, depth = 0) {
48
+ if (depth > 20 || raw == null || shown == null)
49
+ return;
50
+ if (typeof raw === 'string' && typeof shown === 'string' && raw !== shown)
51
+ replacements.set(raw, shown);
52
+ else if (Array.isArray(raw) && Array.isArray(shown))
53
+ raw.forEach((value, i) => collect(value, shown[i], depth + 1));
54
+ else if (typeof raw === 'object' && typeof shown === 'object') {
55
+ for (const [key, value] of Object.entries(raw))
56
+ collect(value, shown[key], depth + 1);
57
+ }
58
+ }
59
+ collect(context, displayData(context));
60
+ if (replacements.size) {
61
+ const pattern = [...replacements.keys()].sort((a, b) => b.length - a.length).map(value => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|');
62
+ text = text.replace(new RegExp(`(?<![\\p{L}\\p{N}_/@.])(?:${pattern})(?![\\p{L}\\p{N}_/@]|\\.[\\p{L}\\p{N}])`, 'gu'), value => replacements.get(value));
63
+ }
64
+ return formatDisplayText(text);
65
+ }
66
+
67
+ export const DISPLAY_TEXT = ['const displayAbbreviations = new Set(' + JSON.stringify([...displayAbbreviations]) + ');', isLiteralField, formatDisplayText, displayData, formatDisplayNarrative].map(value => typeof value === 'function' ? value.toString() : value).join('\n');
@@ -0,0 +1,42 @@
1
+ // Where the agent gateway is, and which token speaks for the buyer.
2
+ //
3
+ // Two small answers the rest of the server shares. The Gateway v2 runtime
4
+ // (src/gateway-v2.mjs) sends the token as a Bearer credential; the hosted
5
+ // OAuth flow (src/oauth.mjs) and the served route index
6
+ // (src/well-known-routes.mjs) read the agent gateway's base URL.
7
+
8
+ /**
9
+ * The agent gateway: OAuth (/v1/oauth/*), connect and the skill. Data requests
10
+ * go to Gateway v2 (src/runtime.mjs), never here.
11
+ */
12
+ export const DEFAULT_GATEWAY_BASE_URL =
13
+ "https://api.apiosk.com/functions/v1/agent-gateway";
14
+
15
+ function trimString(value) {
16
+ return String(value ?? "").trim();
17
+ }
18
+
19
+ export function resolveGatewayBaseUrl(env = process.env) {
20
+ const configured =
21
+ trimString(env.APIOSK_GATEWAY_URL) ||
22
+ trimString(env.APIOSK_GATEWAY_BASE_URL) ||
23
+ trimString(env.APIOSK_BASE_URL);
24
+ return (configured || DEFAULT_GATEWAY_BASE_URL).replace(/\/+$/, "");
25
+ }
26
+
27
+ /**
28
+ * The token that names the buyer to the gateway.
29
+ *
30
+ * A request-scoped token (minted for this OAuth session, stashed on
31
+ * `authInfo.extra` by src/oauth.mjs) always beats the per-process env token:
32
+ * one server serves many buyers, and the caller's own connection is the only
33
+ * one allowed to spend. For stdio, `APIOSK_CONNECT_TOKEN` holds an Apiosk agent
34
+ * token (`apk_access_…` or `apk_live_…`).
35
+ */
36
+ export function resolveConnectToken(authInfo = null, env = process.env) {
37
+ return (
38
+ trimString(authInfo?.extra?.apiosk_connect_token) ||
39
+ trimString(env.APIOSK_CONNECT_TOKEN) ||
40
+ ""
41
+ );
42
+ }
@@ -0,0 +1,50 @@
1
+ // The Ask page's own steps for a connected chatbot. The chatbot fills in the
2
+ // capability object the Ask parser would produce (apiosk_search), then prepares
3
+ // one ranked endpoint with the inputs that endpoint declares (apiosk_prepare).
4
+ // The gateway ranks, prices and quotes; this module only shapes the calls and
5
+ // adds no planner, pricing or balance logic of its own.
6
+ import { randomUUID } from "node:crypto";
7
+ import schemas from "./gateway-v2-contracts.json" with { type: "json" };
8
+
9
+ export const ASK_TOOLS = Object.freeze(["apiosk_search", "apiosk_prepare"]);
10
+
11
+ const SEARCH_NOTICE = "Searching is free and buys nothing. Only candidates with availability \"supported\" can run: fill each required endpoint.inputs field from the person's words or earlier results, ask for anything missing, then call apiosk_prepare with that candidate's endpoint_id, capability and input.";
12
+
13
+ export function askDefinitions(errorFields, taskOutput) {
14
+ const searchOutput = {
15
+ type: "object", additionalProperties: false,
16
+ properties: {
17
+ protocol_version: { type: "string", const: "2" }, view: { type: "string", const: "source_search" },
18
+ parsed_request: { type: "object" }, matches: { type: "array", items: { type: "object", additionalProperties: true } },
19
+ apiosk_sources_searched: { type: "integer", minimum: 0 }, catalog_version: { type: "string" }, notice: { type: "string" }, ...errorFields,
20
+ },
21
+ anyOf: [{ required: ["protocol_version", "view", "parsed_request", "matches", "notice"] }, { required: ["error_code", "message"] }],
22
+ };
23
+ return [
24
+ { name: "apiosk_search", title: "Search sources for a capability object",
25
+ description: "Search Apiosk's sources the way the Ask page does, with a capability object you fill in yourself (parsed_request, following the host instructions' parser rules). Slugs are English dot paths of letters and digits, such as location.time.current. Returns, per capability, the Ask page's ranked sources; each runnable candidate carries endpoint.inputs: the exact input keys, whether each is required, and its value schema for apiosk_prepare. Free: creates no task and buys nothing.",
26
+ inputSchema: schemas.search, outputSchema: searchOutput, annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false } },
27
+ { name: "apiosk_prepare", title: "Prepare one source",
28
+ description: "Prepare one runnable apiosk_search candidate: pass its endpoint_id, capability and an input object whose keys are that candidate's endpoint.inputs fields, filled only from the person's words or earlier results, never placeholders. Returns the same task, price ceiling and approval card as apiosk_discover; nothing is bought until the person approves. Continue with apiosk_execute and apiosk_status.",
29
+ inputSchema: schemas.prepare, outputSchema: taskOutput, annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false } },
30
+ ];
31
+ }
32
+
33
+ /** The gateway request for an Ask tool: its exact wire body, nothing extra. */
34
+ export function askRequest(name, args) {
35
+ if (name === "apiosk_search") {
36
+ // The gateway records who parsed the object; a chatbot parse has no model hash.
37
+ const meta = { model: "connected-chatbot", parsed_at: new Date().toISOString(), text_sha256: "" };
38
+ return { path: "/v2/ask-v2/search", body: { parsed_request: { ...args.parsed_request, meta }, origin: "apiosk" } };
39
+ }
40
+ return { path: "/v2/ask-v2/prepare", body: { request_id: args.request_id || randomUUID(), endpoint_id: args.endpoint_id,
41
+ ...(args.capability && { capability: args.capability }), input: args.input } };
42
+ }
43
+
44
+ /** The card's source search view (`view: "source_search"`). */
45
+ export function presentSearch(result, parsed) {
46
+ if (!Array.isArray(result?.matches)) throw new Error("Unexpected protocol");
47
+ return { protocol_version: "2", view: "source_search", parsed_request: parsed, matches: result.matches,
48
+ ...(Number.isInteger(result.apiosk_sources_searched) && { apiosk_sources_searched: result.apiosk_sources_searched }),
49
+ ...(typeof result.catalog_version === "string" && { catalog_version: result.catalog_version }), notice: SEARCH_NOTICE };
50
+ }
@@ -0,0 +1,23 @@
1
+ export const V2_ACCOUNT_MARKUP = `<nav class="account-links hidden" id="account-shortcuts" aria-label="Apiosk account shortcuts"><button type="button" id="balance-shortcut">Balance</button><button type="button" id="history-shortcut">History</button></nav>`;
2
+
3
+ // Workspace scoping and destinations remain Gateway output, never client input.
4
+ // Identity is deliberately not repeated in the embedded request header.
5
+ // Links are limited to the App origin that owns the plan approval and to the
6
+ // two exact read/manage destinations rendered by this card.
7
+ export const V2_CARD_ACCOUNT = `
8
+ function dataApprovalOrigin(){try{return new URL(output?.proposal?.approval_url||'https://app.apiosk.com').origin}catch{return 'https://app.apiosk.com'}}
9
+ function trustedShortcut(value,path){try{const url=new URL(value),approval=dataApprovalOrigin();return url.protocol==='https:'&&url.pathname===path&&!url.username&&!url.password&&(!approval||url.origin===approval)?url.href:null}catch{return null}}
10
+ function renderAccount(data){
11
+ const bar=byId('account-shortcuts'),workspace=data.context_view?.workspace||data.billing?.workspace;
12
+ if(!workspace?.workspace_id||workspace.kind==='personal'){
13
+ for(const id of ['balance-shortcut','history-shortcut']){const button=byId(id);button.classList.add('hidden');button.onclick=null}
14
+ bar.classList.add('hidden');return
15
+ }
16
+ const links=data.context_view?.navigation||{},balance=trustedShortcut(links.balance_url,'/settings/billing'),history=trustedShortcut(links.history_url,'/usage/history');
17
+ for(const [id,url] of [['balance-shortcut',balance],['history-shortcut',history]]){
18
+ const button=byId(id);button.classList.toggle('hidden',!url);
19
+ button.onclick=url?async()=>{if(!await window.apiosk.openLink(url))showFeedback('Open Apiosk to view this account.','error')}:null
20
+ }
21
+ bar.classList.toggle('hidden',!balance&&!history)
22
+ }
23
+ `;
@@ -0,0 +1,67 @@
1
+ import { approvalFeedback } from './approval-feedback.mjs';
2
+ import { V2_CARD_BUDGET } from './gateway-v2-card-budget.mjs';
3
+ // Runs inside the card: observe consent, then use only gateway-issued actions.
4
+ export const V2_CARD_ACTIONS = `
5
+ let approvalExpiryTimer=null;
6
+ const quoteExpired=data=>Date.parse(data.proposal?.expires_at)<=Date.now();
7
+ ${approvalFeedback.toString()}
8
+ const responseMessage=(data,fallback)=>approvalFeedback(data,money,fallback);
9
+ ${V2_CARD_BUDGET}
10
+ async function refreshPrice(data,button){
11
+ if(busy||output!==data)return;
12
+ busy=true;button.disabled=true;showFeedback('Refreshing the price. No purchase is being made…');
13
+ try{
14
+ const fresh=await window.apiosk.callTool('apiosk_status',{task_ref:data.state.state_ref});
15
+ if(!fresh?.state||fresh.state.state_ref!==data.state.state_ref)throw new Error('Could not recover this request. Check status and try again.');
16
+ if(!quoteExpired(fresh)||fresh.billing?.authorization_active||(fresh.billing?.executions||[]).length){acceptResponse(fresh);return}
17
+ const question=fresh.context_view?.conversation?.at(-1)?.question;
18
+ if(!question)throw new Error('The original question is unavailable. Ask the question again to get a new price.');
19
+ const next=await window.apiosk.callTool('apiosk_discover',{question,state:fresh.state});
20
+ acceptResponse(next);
21
+ if(next?.proposal&&!quoteExpired(next))showFeedback('Price refreshed. Review the plan and click Approve to continue.');
22
+ }catch(e){showFeedback(e&&e.message||'Could not refresh the price. Check status and try again.','error')}
23
+ finally{busy=false;button.disabled=false}
24
+ }
25
+ async function refreshTask(publish=true){if(busy||!output?.state?.state_ref)return;const before=output;if(publish)busy=true;try{const next=await window.apiosk.callTool('apiosk_status',{task_ref:before.state.state_ref});if(!next?.state||next.state.state_ref!==before.state.state_ref)throw new Error(next?.message||'Could not recover this request. Use Check status to try again.');if(publish)acceptResponse(next);else if(!busy&&output===before&&next?.state?.state_ref===before.state.state_ref&&Number(next.state.revision)>=Number(before.state.revision)){render(next);void window.apiosk.context(next).catch(()=>{})}return true}catch(e){if(publish)showFeedback(e&&e.message||'Could not refresh this request. Use Check status to try again.','error')}finally{if(publish)busy=false}}
26
+ async function approvePlan(data,button){
27
+ if(busy)return;
28
+ if(output!==data){showFeedback('This request was updated. Use the current plan below.','error');return}
29
+ if(quoteExpired(data)){render(data);showFeedback('This quote expired. Click Get new price, then review and approve the refreshed plan.','error');return}
30
+ busy=true;button.disabled=true;showFeedback('Saving your approval…');let refusal=null;
31
+ try{
32
+ const next=await window.apiosk.callTool('apiosk_approve',{state:data.state,quote_ref:data.proposal.quote_ref,max_total_atomic:data.proposal.max_total_atomic});
33
+ if(!next?.state||next.state.state_ref!==data.state.state_ref||!next.billing?.authorization_active||next.billing.quote_ref!==data.proposal.quote_ref||next.proposal?.max_total_atomic!==data.proposal.max_total_atomic){refusal=next;throw new Error(responseMessage(next,'Approval was not confirmed. Check status before continuing.'))}
34
+ watchUntil=Date.now()+300000;acceptResponse(next);
35
+ }catch(e){
36
+ watchUntil=0;showFeedback(e&&e.message||'Approval could not be confirmed. Check status before continuing.','error');renderApprovalRecovery(refusal,data);
37
+ }finally{busy=false;button.disabled=false}
38
+ }
39
+ function renderActions(data){
40
+ if(approvalExpiryTimer){clearTimeout(approvalExpiryTimer);approvalExpiryTimer=null}
41
+ const server=data.context_view?.execution_mode==='server';
42
+ const actions=data.next_actions||[],run=actions.find(a=>a.kind==='execute_quoted_step')||(server&&!data.context_view?.worker_active?actions.find(a=>a.kind==='poll'):null),poll=actions.find(a=>a.kind==='poll'),read=data.result||data.context_view?.results?.length?[]:actions.filter(a=>a.kind==='read_result'),cancel=actions.find(a=>a.kind==='cancel');
43
+ const approved=data.billing&&data.billing.authorization_active&&data.billing.quote_ref===(data.proposal&&data.proposal.quote_ref),enabled=data.context_view?.execution_enabled!==false;
44
+ if(run&&planSurface){
45
+ const wrap=el('div','actions plan-actions');
46
+ if(!approved&&enabled&&data.proposal?.approval_url){
47
+ const b=el('button','primary','Approve up to '+money(data.proposal.max_total_atomic,data.proposal.currency,true));
48
+ if(quoteExpired(data)){b.disabled=true;b.textContent='Quote expired — request a new price';const renew=el('button','primary','Get new price');renew.onclick=()=>refreshPrice(data,renew);wrap.append(renew)}
49
+ else{const remaining=Date.parse(data.proposal.expires_at)-Date.now();if(Number.isFinite(remaining)&&remaining>=0)approvalExpiryTimer=setTimeout(()=>{approvalExpiryTimer=null;if(output===data&&!busy){render(data);showFeedback('This quote expired. Get a new price to continue.')}},Math.min(remaining+1,2147483647))}
50
+ b.onclick=data.context_view?.approval_mode==='chatbot'?()=>approvePlan(data,b):async()=>{if(busy)return;watchUntil=Date.now()+300000;const opened=await window.apiosk.openLink(data.proposal.approval_url);showFeedback(opened?'Approve in Apiosk. This card will continue automatically.':'Open Apiosk to approve this request.',opened?'':'error');if(opened)await refreshTask()};wrap.append(b);
51
+ }else if(approved&&enabled&&server){
52
+ if(!attempted.has(run.action_id)){attempted.add(run.action_id);pollTimer=setTimeout(()=>{pollTimer=null;callAction(run)},0)}else{wrap.append(actionButton(run,'Resume request',true))}
53
+ }else if(approved&&enabled&&Date.now()<watchUntil){
54
+ if(attempted.has(run.action_id)||(data.errors||[]).length)wrap.append(actionButton(run,'Retry approved step',true));else wrap.append(el('span','notice','Approved. Continuing your request…'));
55
+ if(!attempted.has(run.action_id)&&!(data.errors||[]).length){pollTimer=setTimeout(()=>{pollTimer=null;if(busy)return;attempted.add(run.action_id);callAction(run)},350)}
56
+ }else if(approved&&enabled){
57
+ const b=el('button','primary','Continue approved request');b.onclick=()=>{watchUntil=Date.now()+300000;return refreshTask()};wrap.append(b);
58
+ }
59
+ if(data.state?.state_ref&&!server){const refresh=el('button','quiet','Check status');refresh.onclick=()=>{watchUntil=Date.now()+300000;return refreshTask()};wrap.append(refresh)}
60
+ if(wrap.children.length)planSurface.append(wrap);
61
+ if(!approved&&enabled&&Date.now()<watchUntil&&Date.parse(data.proposal?.expires_at)>Date.now())pollTimer=setTimeout(()=>{pollTimer=null;refreshTask()},2000);
62
+ }
63
+ if(poll&&!server){const wait=Math.max(350,Number(data.retry_after_ms)||900);pollTimer=setTimeout(()=>{pollTimer=null;callAction(poll)},wait)}
64
+ if(!(data.status==='succeeded'&&!data.context_view?.worker_active&&!(data.errors||[]).length)&&(data.state?.state_ref||read.length||poll||(!server&&cancel))){const s=el('section','section controls'),wrap=el('div','actions');sections.append(s);if(data.state?.state_ref){const check=el('button','quiet','Check status');check.onclick=()=>refreshTask();wrap.append(check)}if(read.length){const view=el('button','quiet','View saved result');view.onclick=()=>refreshTask();wrap.append(view)}if(cancel&&(run||poll||data.context_view?.worker_active))wrap.append(actionButton(cancel,'Cancel request'));s.append(wrap)}
65
+ }
66
+
67
+ `;
@@ -0,0 +1,130 @@
1
+ import { formatDisplayText, formatDisplayNarrative, isLiteralField } from './display-text.mjs';
2
+ import { sourceCurrency } from './source-value-format.mjs';
3
+ import { readBlocks } from './gateway-v2-card-body.mjs';
4
+ // The App's answer text (app/src/lib/domain/gateway-v2-answer.ts and
5
+ // answer-format.ts): every observation, period headings, source notes, every
6
+ // specific limitation and the follow-up question. Display only; the saved
7
+ // analysis and its evidence stay unchanged. Keep in sync with the App.
8
+ const ANSWER_GENERIC_LIMITATIONS = new Set([
9
+ 'Some requested source calls did not return usable data. This analysis is incomplete.',
10
+ 'Some source information is unavailable. The answer uses the available results.',
11
+ 'The returned source response contains no usable records for this question. No finding can be supported from it.',
12
+ ]);
13
+ const ANSWER_PERIOD_KEYS = ['fiscal_year', 'financial_year', 'year', 'fiscalYear', 'financialYear', 'period_end', 'periodEnd', 'fiscal_year_end', 'financialYearEnd', 'reporting_year', 'date_fin_exercice'];
14
+
15
+ function answerRecord(value) { return value && typeof value === 'object' && !Array.isArray(value) ? value : {}; }
16
+ export function answerLocale(question, analysis) {
17
+ const text = (analysis?.observations || []).map(o => o?.text || '').join(' ') || question || '';
18
+ const dutch = text.match(/\b(de|het|een|zijn|voor|van|heeft|bedraagt|jaarrekening|omzet|winst|volgens|toont|werden)\b/gi) ?? [];
19
+ return dutch.length >= 2 ? 'nl-NL' : 'en-US';
20
+ }
21
+ /** The App's formatSourceValue, with its locale: Dutch groups with dots. */
22
+ export function answerValue(value, key = '', currency = null, locale = 'en-US') {
23
+ const raw = String(value ?? '');
24
+ if (isLiteralField(key)) return raw;
25
+ if (typeof value === 'string') value = formatDisplayText(value, key);
26
+ const label = String(key).split('/').at(-1).replace(/[\s_.-]/g, '');
27
+ if (/year|date|code|identifier|kvk|postcode|postal|phone|iban/i.test(label) || /(?:^id$|Id$|ID$|Number$|Nummer$)/.test(label)) return raw;
28
+ if (!/^-?(?:0|[1-9]\d*)(?:\.\d+)?$/.test(raw)) return formatDisplayNarrative(String(value ?? ''));
29
+ const negative = raw.startsWith('-'), unsigned = negative ? raw.slice(1) : raw, [whole, fraction] = unsigned.split('.'), dutch = locale.startsWith('nl');
30
+ const grouped = whole.replace(/\B(?=(\d{3})+(?!\d))/g, dutch ? '.' : ',') + (fraction == null ? '' : (dutch ? ',' : '.') + fraction);
31
+ const monetary = /^(?:(?:total|net|gross))?(Assets|Liabilities|Equity|Receivables|Cash|Property|Provisions|Share|RetainedEarnings|FinancialIncome|Income|Result|Depreciation|EmployeeBenefits|Operating|GrossMargin|Impairment|SumOfExpenses|Revenue|Profit|Turnover|Amount|Balance|Price|Cost|Tax|Bedrag|Omzet|Winst|Kosten)/i.test(label) && !/ratio|percent|rate|count|margin|sharesoutstanding|sharesissued/i.test(label);
32
+ const code = monetary ? sourceCurrency(currency) : null, symbol = code && ({ EUR: '€', USD: '$', GBP: '£' }[code] || code);
33
+ return (negative ? '-' : '') + (symbol ? symbol + ' ' : '') + grouped;
34
+ }
35
+ /** Units and periods next to the exact cited field, never from another result. */
36
+ export function answerValueContext(evidence, results) {
37
+ const pointer = String(evidence?.pointer || ''), doc = (results || []).map(answerRecord).find(d => d.result_ref === evidence?.result_ref), parents = [];
38
+ let node = doc && { ...doc, data: doc.data ?? doc.preview_data };
39
+ for (const part of pointer.split('/').slice(1)) {
40
+ if (!Array.isArray(node)) parents.unshift(answerRecord(node));
41
+ node = node != null && typeof node === 'object' ? node[part.replaceAll('~1', '/').replaceAll('~0', '~')] : undefined;
42
+ }
43
+ const currencyOwner = parents.find(p => p.currency != null || p.currencyCode != null || p.unit != null);
44
+ const period = parents.flatMap(p => ANSWER_PERIOD_KEYS.map(k => p[k])).find(v => /^(?:19|20)\d{2}(?:-\d{2}-\d{2})?$/.test(String(v)));
45
+ return { currency: sourceCurrency(currencyOwner), period: period == null ? null : String(period), field: String(parents[0]?.key ?? pointer.split('/').at(-1) ?? '') };
46
+ }
47
+ export function formatAnswerObservation(observation, results, locale) {
48
+ const values = new Map(), evidence = Array.isArray(observation.evidence) ? observation.evidence : [];
49
+ for (const item of evidence) {
50
+ const raw = String(item?.value);
51
+ if (!/^-?(?:0|[1-9]\d*)(?:\.\d+)?$/.test(raw)) continue;
52
+ const context = answerValueContext(item, results), field = item.field ?? context.field, pointer = String(item.pointer || ''), parent = pointer.slice(0, pointer.lastIndexOf('/'));
53
+ const currency = context.currency ?? sourceCurrency(evidence.find(e => e?.result_ref === item.result_ref && e.pointer === parent + '/currency')?.value);
54
+ const display = answerValue(item.value, field, currency, locale), previous = values.get(raw);
55
+ values.set(raw, values.has(raw) && previous?.display !== display ? null : { display, currency });
56
+ }
57
+ const codes = 'EUR|USD|GBP|CHF|JPY|CAD|AUD|SEK|NOK|DKK';
58
+ const pattern = new RegExp(`(?<![\\p{L}\\p{N}_/@.:$€£+\\-])(?:(${codes}|[$€£])\\s*)?(-?(?:0|[1-9]\\d*)(?:\\.\\d+)?)(?:\\s*(${codes})(?!\\p{L}))?(?![\\p{L}\\p{N}_/@]|[.:,\\-]\\d)`, 'gu');
59
+ return String(observation.text).replace(pattern, (match, prefix, raw, suffix) => {
60
+ const value = values.get(raw);
61
+ if (!value) return match;
62
+ const explicit = prefix ?? suffix;
63
+ if (!explicit) return value.display;
64
+ const code = { '$': 'USD', '€': 'EUR', '£': 'GBP' }[explicit] ?? explicit;
65
+ return value.currency && value.currency !== code ? match : answerValue(raw, 'Amount', code, locale);
66
+ });
67
+ }
68
+ /** A heading may name only a period this observation actually cites. */
69
+ export function observationPeriod(observation, results) {
70
+ const periods = (observation.evidence || []).flatMap(e => {
71
+ const key = String(e?.field ?? e?.pointer ?? '').split('/').at(-1) ?? '';
72
+ if (ANSWER_PERIOD_KEYS.some(k => k.toLowerCase() === key.toLowerCase()) && /^(?:19|20)\d{2}(?:-\d{2}-\d{2})?$/.test(String(e.value))) return [String(e.value)];
73
+ const context = answerValueContext(e, results);
74
+ return context.period ? [context.period] : [];
75
+ });
76
+ const unique = [...new Set(periods)];
77
+ return unique.length === 1 ? unique[0] : null;
78
+ }
79
+ export function answerText(analysis, context) {
80
+ if (!analysis) return '';
81
+ const question = context?.question || '', results = context?.results || [], locale = answerLocale(question, analysis), nl = locale === 'nl-NL';
82
+ const observations = (analysis.observations || []).filter(o => typeof o?.text === 'string' && o.text.trim());
83
+ const limitations = (analysis.limitations || []).filter(text => typeof text === 'string' && text.trim() && !ANSWER_GENERIC_LIMITATIONS.has(text));
84
+ const structured = observations.length > 1 && !/\b(one sentence|single sentence|één zin|een zin)\b/i.test(question);
85
+ const blocks = [];
86
+ let previousHeading = null;
87
+ for (const observation of observations) {
88
+ if (structured) {
89
+ const period = observationPeriod(observation, results);
90
+ const heading = period ? (nl ? 'Verslagperiode ' : 'Reporting period ') + period : nl ? 'Belangrijkste bevindingen' : 'Key findings';
91
+ if (heading !== previousHeading) blocks.push('## ' + heading);
92
+ previousHeading = heading;
93
+ }
94
+ blocks.push(formatAnswerObservation(observation, results, locale));
95
+ }
96
+ if (limitations.length) {
97
+ if (observations.length) blocks.push('## ' + (nl ? 'Bronnen en beperkingen' : 'Source notes'));
98
+ blocks.push(...new Set(limitations));
99
+ }
100
+ if (analysis.follow_up_question) blocks.push(analysis.follow_up_question);
101
+ return blocks.length ? blocks.join('\n\n') : [...new Set((analysis.limitations || []).filter(text => typeof text === 'string'))].join('\n\n');
102
+ }
103
+ export function answerDocumentRequested(analysis, question) {
104
+ return analysis?.presentation?.kind === 'document' || /\b(document|pdf|rapport|report|dossier)\b/i.test(question || '');
105
+ }
106
+
107
+ // ---- DOM, run inside the card only --------------------------------------------
108
+ function answerInline(node, value) {
109
+ const parts = String(value).split(/(-?(?:[$€£]|USD|EUR|GBP|CHF|JPY|CAD|AUD|SEK|NOK|DKK)\s-?\d[\d.,]*)/g);
110
+ if (parts.length === 1) { node.textContent = value; return node; }
111
+ parts.forEach((part, index) => { if (part) node.append(el(index % 2 ? 'strong' : 'span', index % 2 ? 'answer-amount' : '', part)); });
112
+ return node;
113
+ }
114
+ /** The App's StreamingReply, settled: headings, paragraphs and lists. */
115
+ function renderAnswerText(value, nameContext) {
116
+ const wrap = el('div', 'answer-text');
117
+ for (const block of readBlocks(formatDisplayNarrative(value, nameContext))) {
118
+ if (block.kind === 'heading') { wrap.append(el('h3', 'answer-heading', block.text)); continue; }
119
+ if (block.kind === 'paragraph') { wrap.append(answerInline(el('p'), block.text)); continue; }
120
+ const list = el(block.kind === 'numbers' ? 'ol' : 'ul');
121
+ for (const item of block.items) list.append(answerInline(el('li'), item));
122
+ wrap.append(list);
123
+ }
124
+ return wrap;
125
+ }
126
+
127
+ export const V2_CARD_ANSWER_TEXT = [
128
+ `const ANSWER_GENERIC_LIMITATIONS=new Set(${JSON.stringify([...ANSWER_GENERIC_LIMITATIONS])}),ANSWER_PERIOD_KEYS=${JSON.stringify(ANSWER_PERIOD_KEYS)};`,
129
+ answerRecord, answerLocale, answerValue, answerValueContext, formatAnswerObservation, observationPeriod, answerText, answerDocumentRequested, answerInline, renderAnswerText,
130
+ ].map(value => typeof value === 'function' ? value.toString() : value).join('\n');