@foldrun/core 0.2.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 (166) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +239 -0
  3. package/dist/index.d.ts +32 -0
  4. package/dist/index.js +43 -0
  5. package/dist/src/agent-tools.d.ts +31 -0
  6. package/dist/src/agent-tools.js +99 -0
  7. package/dist/src/agents-md.d.ts +9 -0
  8. package/dist/src/agents-md.js +83 -0
  9. package/dist/src/api-tools.d.ts +38 -0
  10. package/dist/src/api-tools.js +313 -0
  11. package/dist/src/approvals.d.ts +39 -0
  12. package/dist/src/approvals.js +103 -0
  13. package/dist/src/arrange.d.ts +8 -0
  14. package/dist/src/arrange.js +40 -0
  15. package/dist/src/catalog.d.ts +108 -0
  16. package/dist/src/catalog.js +255 -0
  17. package/dist/src/completions.d.ts +61 -0
  18. package/dist/src/completions.js +578 -0
  19. package/dist/src/confine.d.ts +31 -0
  20. package/dist/src/confine.js +298 -0
  21. package/dist/src/container.d.ts +30 -0
  22. package/dist/src/container.js +172 -0
  23. package/dist/src/context-tools.d.ts +81 -0
  24. package/dist/src/context-tools.js +347 -0
  25. package/dist/src/deploy.d.ts +100 -0
  26. package/dist/src/deploy.js +377 -0
  27. package/dist/src/egress.d.ts +75 -0
  28. package/dist/src/egress.js +133 -0
  29. package/dist/src/evals.d.ts +150 -0
  30. package/dist/src/evals.js +651 -0
  31. package/dist/src/flow-lint.d.ts +16 -0
  32. package/dist/src/flow-lint.js +177 -0
  33. package/dist/src/git-hook.d.ts +1 -0
  34. package/dist/src/git-hook.js +70 -0
  35. package/dist/src/git.d.ts +37 -0
  36. package/dist/src/git.js +101 -0
  37. package/dist/src/gitrepo.d.ts +95 -0
  38. package/dist/src/gitrepo.js +355 -0
  39. package/dist/src/history.d.ts +82 -0
  40. package/dist/src/history.js +259 -0
  41. package/dist/src/kinds.d.ts +95 -0
  42. package/dist/src/kinds.js +421 -0
  43. package/dist/src/library.d.ts +94 -0
  44. package/dist/src/library.js +341 -0
  45. package/dist/src/linkable.d.ts +20 -0
  46. package/dist/src/linkable.js +113 -0
  47. package/dist/src/model-probe.d.ts +21 -0
  48. package/dist/src/model-probe.js +81 -0
  49. package/dist/src/notify.d.ts +61 -0
  50. package/dist/src/notify.js +252 -0
  51. package/dist/src/oauth-presets.d.ts +13 -0
  52. package/dist/src/oauth-presets.js +39 -0
  53. package/dist/src/okf.d.ts +220 -0
  54. package/dist/src/okf.js +656 -0
  55. package/dist/src/openapi.d.ts +66 -0
  56. package/dist/src/openapi.js +342 -0
  57. package/dist/src/paths.d.ts +33 -0
  58. package/dist/src/paths.js +78 -0
  59. package/dist/src/platform.d.ts +32 -0
  60. package/dist/src/platform.js +48 -0
  61. package/dist/src/providers.d.ts +47 -0
  62. package/dist/src/providers.js +114 -0
  63. package/dist/src/refs.d.ts +20 -0
  64. package/dist/src/refs.js +78 -0
  65. package/dist/src/run-container.d.ts +227 -0
  66. package/dist/src/run-container.js +760 -0
  67. package/dist/src/runner.d.ts +241 -0
  68. package/dist/src/runner.js +3215 -0
  69. package/dist/src/runtime.d.ts +41 -0
  70. package/dist/src/runtime.js +357 -0
  71. package/dist/src/script-tools.d.ts +62 -0
  72. package/dist/src/script-tools.js +296 -0
  73. package/dist/src/secret-files.d.ts +12 -0
  74. package/dist/src/secret-files.js +92 -0
  75. package/dist/src/secrets.d.ts +147 -0
  76. package/dist/src/secrets.js +626 -0
  77. package/dist/src/starter.d.ts +21 -0
  78. package/dist/src/starter.js +399 -0
  79. package/dist/src/step-exec.d.ts +136 -0
  80. package/dist/src/step-exec.js +391 -0
  81. package/dist/src/storage.d.ts +123 -0
  82. package/dist/src/storage.js +640 -0
  83. package/dist/src/store.d.ts +1052 -0
  84. package/dist/src/store.js +2378 -0
  85. package/dist/src/tar.d.ts +30 -0
  86. package/dist/src/tar.js +111 -0
  87. package/dist/src/tool-names.d.ts +39 -0
  88. package/dist/src/tool-names.js +79 -0
  89. package/dist/src/tool-programs.d.ts +18 -0
  90. package/dist/src/tool-programs.js +52 -0
  91. package/dist/src/tool-test.d.ts +19 -0
  92. package/dist/src/tool-test.js +335 -0
  93. package/dist/src/translator.d.ts +159 -0
  94. package/dist/src/translator.js +986 -0
  95. package/dist/src/triggers.d.ts +60 -0
  96. package/dist/src/triggers.js +270 -0
  97. package/dist/src/webhook.d.ts +47 -0
  98. package/dist/src/webhook.js +148 -0
  99. package/index.ts +44 -0
  100. package/package.json +64 -0
  101. package/src/agent-tools.ts +138 -0
  102. package/src/agents-md.ts +81 -0
  103. package/src/api-tools.ts +389 -0
  104. package/src/approvals.ts +129 -0
  105. package/src/arrange.ts +42 -0
  106. package/src/catalog.ts +321 -0
  107. package/src/completions.ts +652 -0
  108. package/src/confine.ts +350 -0
  109. package/src/container.ts +220 -0
  110. package/src/context-tools.ts +435 -0
  111. package/src/deploy.ts +458 -0
  112. package/src/egress.ts +176 -0
  113. package/src/evals.ts +812 -0
  114. package/src/flow-lint.ts +215 -0
  115. package/src/git-hook.ts +73 -0
  116. package/src/git.ts +117 -0
  117. package/src/gitrepo.ts +391 -0
  118. package/src/history.ts +329 -0
  119. package/src/kinds.ts +507 -0
  120. package/src/library.ts +419 -0
  121. package/src/linkable.ts +123 -0
  122. package/src/model-probe.ts +112 -0
  123. package/src/notify.ts +281 -0
  124. package/src/oauth-presets.ts +54 -0
  125. package/src/okf.ts +768 -0
  126. package/src/openapi.ts +383 -0
  127. package/src/paths.ts +79 -0
  128. package/src/platform.ts +87 -0
  129. package/src/providers.ts +147 -0
  130. package/src/refs.ts +83 -0
  131. package/src/run-container.ts +883 -0
  132. package/src/runner.ts +3446 -0
  133. package/src/runtime.ts +404 -0
  134. package/src/script-tools.ts +383 -0
  135. package/src/secret-files.ts +101 -0
  136. package/src/secrets.ts +799 -0
  137. package/src/starter.ts +415 -0
  138. package/src/step-exec.ts +498 -0
  139. package/src/storage.ts +771 -0
  140. package/src/store.ts +2985 -0
  141. package/src/tar.ts +124 -0
  142. package/src/tool-names.ts +91 -0
  143. package/src/tool-programs.ts +65 -0
  144. package/src/tool-test.ts +401 -0
  145. package/src/translator.ts +1029 -0
  146. package/src/triggers.ts +287 -0
  147. package/src/webhook.ts +183 -0
  148. package/templates/blog-desk/AGENTS.md +13 -0
  149. package/templates/blog-desk/agents/researcher/agent.md +11 -0
  150. package/templates/blog-desk/agents/writer/agent.md +8 -0
  151. package/templates/blog-desk/evals/writer-quality.md +10 -0
  152. package/templates/blog-desk/flows/publish.md +10 -0
  153. package/templates/blog-desk/knowledge/house-style.md +9 -0
  154. package/templates/blog-desk/knowledge/index.md +9 -0
  155. package/templates/hello/AGENTS.md +18 -0
  156. package/templates/hello/agents/curator/agent.md +32 -0
  157. package/templates/hello/agents/notetaker/agent.md +24 -0
  158. package/templates/hello/evals/house-style.md +16 -0
  159. package/templates/hello/flows/memory-review.md +7 -0
  160. package/templates/hello/flows/note.md +7 -0
  161. package/templates/hello/knowledge/house-style.md +15 -0
  162. package/templates/hello/knowledge/index.md +9 -0
  163. package/templates/hello/skills/plain-english/SKILL.md +11 -0
  164. package/templates/hello/state/publishing.md +7 -0
  165. package/templates/hello/tools/wordcount/run.py +13 -0
  166. package/templates/hello/tools/wordcount/tool.md +22 -0
@@ -0,0 +1,147 @@
1
+ // The providers this runtime knows by name.
2
+ //
3
+ // provider:
4
+ // name: groq
5
+ // token: ${GROQ_API_KEY}
6
+ // models: { fast: llama-3.3-70b-versatile }
7
+ //
8
+ // A name resolves to three facts: which wire format the endpoint speaks,
9
+ // where it is, and which header the key travels in. Everything here is
10
+ // bring-your-own-key — the platform never holds a provider credential on a
11
+ // customer's behalf — and a name is a convenience over `base_url:` +
12
+ // `format:` + `auth:`, never a requirement: an endpoint this table has
13
+ // never heard of works the same way by spelling those three out.
14
+ //
15
+ // Three formats. `responses` is OpenAI's newer API — the same translator
16
+ // with a second pair of mappings, for an OpenAI key that wants what only
17
+ // that shape carries (a PDF as input, the reasoning knobs). Nobody else
18
+ // implements it, so no preset defaults to it; a block asks for it by name.
19
+ // Two formats, deliberately. `anthropic` is what the runtime speaks, so
20
+ // those endpoints are reached directly. `openai` endpoints are reached
21
+ // through the runtime's own translator (translator.ts), which runs on
22
+ // localhost inside the run sandbox and rewrites Anthropic Messages to Chat
23
+ // Completions and back. Every provider without an Anthropic-shaped endpoint
24
+ // speaks Chat Completions — OpenAI, Gemini, xAI, Groq, Mistral, Hugging
25
+ // Face, Cloudflare's own models — so one translation reaches all of them.
26
+ //
27
+ // URLs and header shapes were checked against each provider's own
28
+ // documentation on 2026-09-02 and again on 2026-09-06 (see docs/providers.md). `verified` says
29
+ // whether a tool loop was actually driven through the endpoint from here,
30
+ // which is the only claim that matters for an agent runtime; a name without
31
+ // it is documented, not proven.
32
+
33
+ export type WireFormat = "anthropic" | "openai" | "responses";
34
+
35
+ /** Where the key goes for an Anthropic-format endpoint. A translated
36
+ * endpoint always gets a bearer token, because the translator is the
37
+ * client there and the SDK's header shape never reaches the provider. */
38
+ export type AuthShape = "bearer" | "x-api-key";
39
+
40
+ export interface ProviderPreset {
41
+ /** The name people write and the docs use. */
42
+ name: string;
43
+ /** How it reads on a run trace and in the help page. */
44
+ title: string;
45
+ format: WireFormat;
46
+ /** Absent when the customer's own account decides it (a workspace id in
47
+ * the host, an account id in the path): then `base_url:` is required. */
48
+ baseUrl?: string;
49
+ auth: AuthShape;
50
+ /** `max_completion_tokens` for endpoints that reject `max_tokens` on
51
+ * reasoning models; everything else takes the classic name. */
52
+ maxTokensParam?: "max_tokens" | "max_completion_tokens";
53
+ /** Whether Anthropic `thinking:` should become OpenAI `reasoning_effort`.
54
+ * Off means the translator drops thinking rather than send a parameter
55
+ * the endpoint would reject. */
56
+ reasoningEffort?: boolean;
57
+ /** One line a person should read before relying on it. */
58
+ note?: string;
59
+ /** A tool loop was driven through this endpoint from this runtime. */
60
+ verified?: boolean;
61
+ }
62
+
63
+ export const PROVIDERS: readonly ProviderPreset[] = [
64
+ // ------------------------------------------------ Anthropic-shaped, direct
65
+ { name: "anthropic", title: "Anthropic", format: "anthropic", baseUrl: "https://api.anthropic.com", auth: "x-api-key", verified: true,
66
+ note: "Models newer than Opus 4.6 reject top_k with a 400; the runtime never sends it unless a params: block does." },
67
+ { name: "openrouter", title: "OpenRouter", format: "anthropic", baseUrl: "https://openrouter.ai/api", auth: "bearer", verified: true,
68
+ note: "Hundreds of models behind one key. Its own docs disagree on how well non-Anthropic models hold a tool loop on this endpoint — probe the model you mean to use." },
69
+ { name: "deepseek", title: "DeepSeek", format: "anthropic", baseUrl: "https://api.deepseek.com/anthropic", auth: "x-api-key",
70
+ note: "Ignores top_k, cache_control and thinking budgets; Claude model names are remapped to DeepSeek's." },
71
+ { name: "kimi", title: "Moonshot Kimi", format: "anthropic", baseUrl: "https://api.moonshot.ai/anthropic", auth: "bearer",
72
+ note: "Own model ids only. Known bug (2026-09): K3 reuses one tool_use id across separate calls, which breaks a tool loop." },
73
+ { name: "moonshot", title: "Moonshot Kimi", format: "anthropic", baseUrl: "https://api.moonshot.ai/anthropic", auth: "bearer",
74
+ note: "Same endpoint as kimi." },
75
+ { name: "zai", title: "z.ai (GLM)", format: "anthropic", baseUrl: "https://api.z.ai/api/anthropic", auth: "bearer" },
76
+ { name: "minimax", title: "MiniMax", format: "anthropic", baseUrl: "https://api.minimax.io/anthropic", auth: "bearer" },
77
+ { name: "qwen", title: "Alibaba Qwen (Model Studio)", format: "anthropic", auth: "x-api-key",
78
+ note: "base_url depends on the plan: pay-as-you-go https://<workspace>.<region>.maas.aliyuncs.com/apps/anthropic; Coding Plan https://coding-intl.dashscope.aliyuncs.com/apps/anthropic. Own model ids (qwen3.7-max …), no remap; no reasoning_effort, use thinking." },
79
+ { name: "fireworks", title: "Fireworks AI", format: "anthropic", baseUrl: "https://api.fireworks.ai/inference", auth: "bearer",
80
+ note: "No server-side tools; no adaptive thinking." },
81
+ { name: "deepinfra", title: "DeepInfra", format: "anthropic", baseUrl: "https://api.deepinfra.com/anthropic", auth: "bearer" },
82
+ { name: "sambanova", title: "SambaNova", format: "anthropic", baseUrl: "https://api.sambanova.ai", auth: "x-api-key",
83
+ note: "No server-side tools, base64 images only." },
84
+ { name: "vercel", title: "Vercel AI Gateway", format: "anthropic", baseUrl: "https://ai-gateway.vercel.sh", auth: "bearer",
85
+ note: "Model ids are namespaced, e.g. openai/gpt-5." },
86
+ { name: "litellm", title: "LiteLLM (yours)", format: "anthropic", auth: "x-api-key",
87
+ note: "base_url is your proxy, e.g. http://litellm.internal:4000. It presents an Anthropic endpoint and speaks anything behind it." },
88
+ { name: "cloudflare-gateway", title: "Cloudflare AI Gateway", format: "anthropic", auth: "x-api-key",
89
+ note: "base_url is https://gateway.ai.cloudflare.com/v1/<account>/<gateway>/anthropic — a pass-through to Anthropic with logging; for Cloudflare's own models use name: cloudflare." },
90
+ { name: "ollama", title: "Ollama (local)", format: "anthropic", baseUrl: "http://localhost:11434", auth: "x-api-key",
91
+ note: "v0.14+. Ignores tool_choice and cache_control; accepts but does not enforce thinking budgets; base64 images only. Any key value is accepted." },
92
+ { name: "lmstudio", title: "LM Studio (local)", format: "anthropic", baseUrl: "http://localhost:1234", auth: "x-api-key" },
93
+ { name: "vllm", title: "vLLM (yours)", format: "anthropic", auth: "bearer",
94
+ note: "base_url is your server (default :8000). Start it with --enable-auto-tool-choice and a --tool-call-parser or no tool loop closes." },
95
+
96
+ // ------------------------------------------- Chat-Completions, translated
97
+ { name: "openai", title: "OpenAI", format: "openai", baseUrl: "https://api.openai.com/v1", auth: "bearer", verified: true,
98
+ maxTokensParam: "max_completion_tokens", reasoningEffort: true,
99
+ note: "Reached over Chat Completions, which OpenAI keeps supporting; the Responses API is not spoken here, so a reasoning model's reasoning does not carry across tool calls." },
100
+ { name: "gemini", title: "Google Gemini", format: "openai", baseUrl: "https://generativelanguage.googleapis.com/v1beta/openai", auth: "bearer",
101
+ note: "Google's OpenAI-compatible route. Unknown parameters are ignored silently; reasoning cannot be switched off on the newest models." },
102
+ { name: "xai", title: "xAI Grok", format: "openai", baseUrl: "https://api.x.ai/v1", auth: "bearer",
103
+ maxTokensParam: "max_completion_tokens", reasoningEffort: true },
104
+ { name: "groq", title: "Groq", format: "openai", baseUrl: "https://api.groq.com/openai/v1", auth: "bearer",
105
+ maxTokensParam: "max_completion_tokens", reasoningEffort: true },
106
+ { name: "mistral", title: "Mistral", format: "openai", baseUrl: "https://api.mistral.ai/v1", auth: "bearer" },
107
+ { name: "together", title: "Together AI", format: "openai", baseUrl: "https://api.together.ai/v1", auth: "bearer" },
108
+ { name: "cerebras", title: "Cerebras", format: "openai", baseUrl: "https://api.cerebras.ai/v1", auth: "bearer",
109
+ maxTokensParam: "max_completion_tokens", reasoningEffort: true },
110
+ { name: "huggingface", title: "Hugging Face", format: "openai", baseUrl: "https://router.huggingface.co/v1", auth: "bearer",
111
+ note: "The Inference Providers router; model ids are Hub ids, e.g. meta-llama/Llama-3.3-70B-Instruct." },
112
+ { name: "cloudflare", title: "Cloudflare Workers AI", format: "openai", auth: "bearer",
113
+ note: "base_url is https://api.cloudflare.com/client/v4/accounts/<account>/ai/v1" },
114
+ { name: "nebius", title: "Nebius Token Factory", format: "openai", baseUrl: "https://api.tokenfactory.nebius.com/v1", auth: "bearer" },
115
+ { name: "novita", title: "Novita", format: "openai", baseUrl: "https://api.novita.ai/openai", auth: "bearer",
116
+ note: "Also has an Anthropic-shaped route at https://api.novita.ai/anthropic — spell it out with format: anthropic to skip the translator (header shape unverified)." },
117
+ { name: "hyperbolic", title: "Hyperbolic", format: "openai", baseUrl: "https://api.hyperbolic.xyz/v1", auth: "bearer" },
118
+ ];
119
+
120
+ /**
121
+ * Request fields `params:` may not set.
122
+ *
123
+ * These are not the provider's knobs, they are the conversation: the
124
+ * messages, the tools the agent declared, whether the reply streams, and
125
+ * which model the tier resolved to. A file that could overwrite them could
126
+ * silently break the agent loop — or make `model: fast` mean something else
127
+ * — from a line that looks like a tuning option. Everything else passes
128
+ * through untouched, because a vendor's own parameters are the vendor's
129
+ * business and this format will never model them.
130
+ */
131
+ export const PROTECTED_PARAMS = ["messages", "tools", "stream", "stream_options", "model"] as const;
132
+
133
+ /** A preset by any spelling a person reaches for: case-insensitive, and
134
+ * "z.ai" / "z-ai" / "hugging-face" all land. */
135
+ export function providerPreset(name: unknown): ProviderPreset | null {
136
+ if (typeof name !== "string") return null;
137
+ const key = name.trim().toLowerCase().replace(/[^a-z0-9]/g, "");
138
+ if (!key) return null;
139
+ return PROVIDERS.find((p) => p.name.replace(/[^a-z0-9]/g, "") === key) ?? null;
140
+ }
141
+
142
+ /** Does this URL look like a Chat-Completions endpoint a person pasted
143
+ * without saying `format: openai`? The heuristic the checker uses to say
144
+ * so before a run fails with a 404 from the wrong path. */
145
+ export function looksOpenAiShaped(baseUrl: string): boolean {
146
+ return /\/v1\/?$|\/chat\/completions|api\.openai\.com|\/openai\b/i.test(baseUrl);
147
+ }
package/src/refs.ts ADDED
@@ -0,0 +1,83 @@
1
+ // A frontmatter list that names files — `tools:`, `skills:`, `agents:`,
2
+ // `after:` — read the same way whether an entry is bare or a `[[link]]`.
3
+ //
4
+ // `[[name]]` is the format's one reference syntax, and until now the
5
+ // frontmatter was the last place a name had to be bare. The rule is one
6
+ // sentence: if it names a file, you can link it. Agents, flows, skills,
7
+ // tools, knowledge and memory are files; secrets, model and size are values.
8
+ //
9
+ // YAML is the complication. To YAML, `- [[site_repo]]` is a nested list
10
+ // (`[["site_repo"]]`), and `tools: [read, [[site_repo]]]` is a list holding
11
+ // a string and a list. So an entry that arrives as an array IS a link — the
12
+ // brackets were consumed by the parser — and one that arrives as the string
13
+ // "[[x]]" (quoted, or from a step option line) is the same link. Both read
14
+ // as the name with `linked: true`; a bare string reads as `linked: false`.
15
+ // The distinction matters once: a linked name can only mean one of the
16
+ // author's files, so `[[search]]` is your tool and `search` is the runtime's
17
+ // group. Everywhere else the two are the same name.
18
+
19
+ /** One entry of a file-naming list. `mode` is the value of a map-form
20
+ * entry — `{bash: ask}` — and null for a plain name. */
21
+ export interface Ref {
22
+ name: string;
23
+ linked: boolean;
24
+ mode: string | null;
25
+ }
26
+
27
+ /** The name inside `[[…]]`, with `flow:` and surrounding space kept. */
28
+ export function stripLink(raw: string): { name: string; linked: boolean } {
29
+ const s = raw.trim();
30
+ const m = s.match(/^\[\[(.*)\]\]$/);
31
+ return m ? { name: m[1].trim(), linked: true } : { name: s, linked: false };
32
+ }
33
+
34
+ /** The first string anywhere inside a nested array — what YAML made of
35
+ * `[[x]]`. Deeper nesting (`[[[x]]]`) collapses to the same name. */
36
+ function innermost(value: unknown): string | null {
37
+ if (typeof value === "string" || typeof value === "number") return String(value);
38
+ if (Array.isArray(value)) {
39
+ for (const v of value) {
40
+ const s = innermost(v);
41
+ if (s !== null) return s;
42
+ }
43
+ }
44
+ return null;
45
+ }
46
+
47
+ /**
48
+ * Every entry of a list field, in order, empty names dropped. Accepts the
49
+ * shapes a file can carry: an inline or block list of names, links or maps;
50
+ * a single name; or Claude Code's comma string (`tools: Read, Grep`).
51
+ */
52
+ export function refList(raw: unknown): Ref[] {
53
+ if (raw === undefined || raw === null) return [];
54
+ const entries: unknown[] =
55
+ Array.isArray(raw) ? raw : typeof raw === "string" && raw.includes(",") ? raw.split(",") : [raw];
56
+ const out: Ref[] = [];
57
+ for (const entry of entries) {
58
+ if (entry === null || entry === undefined) continue;
59
+ if (Array.isArray(entry)) {
60
+ const name = innermost(entry);
61
+ if (name !== null) {
62
+ const inner = stripLink(name);
63
+ if (inner.name) out.push({ name: inner.name, linked: true, mode: null });
64
+ }
65
+ continue;
66
+ }
67
+ if (typeof entry === "object") {
68
+ const [key, value] = Object.entries(entry as Record<string, unknown>)[0] ?? [];
69
+ if (!key) continue;
70
+ const { name, linked } = stripLink(key);
71
+ if (name) out.push({ name, linked, mode: value === undefined || value === null ? null : String(value) });
72
+ continue;
73
+ }
74
+ const { name, linked } = stripLink(String(entry));
75
+ if (name) out.push({ name, linked, mode: null });
76
+ }
77
+ return out;
78
+ }
79
+
80
+ /** Just the names — for a reader that does not care how they were spelt. */
81
+ export function refNames(raw: unknown): string[] {
82
+ return refList(raw).map((r) => r.name);
83
+ }