@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.
- package/LICENSE +202 -0
- package/README.md +239 -0
- package/dist/index.d.ts +32 -0
- package/dist/index.js +43 -0
- package/dist/src/agent-tools.d.ts +31 -0
- package/dist/src/agent-tools.js +99 -0
- package/dist/src/agents-md.d.ts +9 -0
- package/dist/src/agents-md.js +83 -0
- package/dist/src/api-tools.d.ts +38 -0
- package/dist/src/api-tools.js +313 -0
- package/dist/src/approvals.d.ts +39 -0
- package/dist/src/approvals.js +103 -0
- package/dist/src/arrange.d.ts +8 -0
- package/dist/src/arrange.js +40 -0
- package/dist/src/catalog.d.ts +108 -0
- package/dist/src/catalog.js +255 -0
- package/dist/src/completions.d.ts +61 -0
- package/dist/src/completions.js +578 -0
- package/dist/src/confine.d.ts +31 -0
- package/dist/src/confine.js +298 -0
- package/dist/src/container.d.ts +30 -0
- package/dist/src/container.js +172 -0
- package/dist/src/context-tools.d.ts +81 -0
- package/dist/src/context-tools.js +347 -0
- package/dist/src/deploy.d.ts +100 -0
- package/dist/src/deploy.js +377 -0
- package/dist/src/egress.d.ts +75 -0
- package/dist/src/egress.js +133 -0
- package/dist/src/evals.d.ts +150 -0
- package/dist/src/evals.js +651 -0
- package/dist/src/flow-lint.d.ts +16 -0
- package/dist/src/flow-lint.js +177 -0
- package/dist/src/git-hook.d.ts +1 -0
- package/dist/src/git-hook.js +70 -0
- package/dist/src/git.d.ts +37 -0
- package/dist/src/git.js +101 -0
- package/dist/src/gitrepo.d.ts +95 -0
- package/dist/src/gitrepo.js +355 -0
- package/dist/src/history.d.ts +82 -0
- package/dist/src/history.js +259 -0
- package/dist/src/kinds.d.ts +95 -0
- package/dist/src/kinds.js +421 -0
- package/dist/src/library.d.ts +94 -0
- package/dist/src/library.js +341 -0
- package/dist/src/linkable.d.ts +20 -0
- package/dist/src/linkable.js +113 -0
- package/dist/src/model-probe.d.ts +21 -0
- package/dist/src/model-probe.js +81 -0
- package/dist/src/notify.d.ts +61 -0
- package/dist/src/notify.js +252 -0
- package/dist/src/oauth-presets.d.ts +13 -0
- package/dist/src/oauth-presets.js +39 -0
- package/dist/src/okf.d.ts +220 -0
- package/dist/src/okf.js +656 -0
- package/dist/src/openapi.d.ts +66 -0
- package/dist/src/openapi.js +342 -0
- package/dist/src/paths.d.ts +33 -0
- package/dist/src/paths.js +78 -0
- package/dist/src/platform.d.ts +32 -0
- package/dist/src/platform.js +48 -0
- package/dist/src/providers.d.ts +47 -0
- package/dist/src/providers.js +114 -0
- package/dist/src/refs.d.ts +20 -0
- package/dist/src/refs.js +78 -0
- package/dist/src/run-container.d.ts +227 -0
- package/dist/src/run-container.js +760 -0
- package/dist/src/runner.d.ts +241 -0
- package/dist/src/runner.js +3215 -0
- package/dist/src/runtime.d.ts +41 -0
- package/dist/src/runtime.js +357 -0
- package/dist/src/script-tools.d.ts +62 -0
- package/dist/src/script-tools.js +296 -0
- package/dist/src/secret-files.d.ts +12 -0
- package/dist/src/secret-files.js +92 -0
- package/dist/src/secrets.d.ts +147 -0
- package/dist/src/secrets.js +626 -0
- package/dist/src/starter.d.ts +21 -0
- package/dist/src/starter.js +399 -0
- package/dist/src/step-exec.d.ts +136 -0
- package/dist/src/step-exec.js +391 -0
- package/dist/src/storage.d.ts +123 -0
- package/dist/src/storage.js +640 -0
- package/dist/src/store.d.ts +1052 -0
- package/dist/src/store.js +2378 -0
- package/dist/src/tar.d.ts +30 -0
- package/dist/src/tar.js +111 -0
- package/dist/src/tool-names.d.ts +39 -0
- package/dist/src/tool-names.js +79 -0
- package/dist/src/tool-programs.d.ts +18 -0
- package/dist/src/tool-programs.js +52 -0
- package/dist/src/tool-test.d.ts +19 -0
- package/dist/src/tool-test.js +335 -0
- package/dist/src/translator.d.ts +159 -0
- package/dist/src/translator.js +986 -0
- package/dist/src/triggers.d.ts +60 -0
- package/dist/src/triggers.js +270 -0
- package/dist/src/webhook.d.ts +47 -0
- package/dist/src/webhook.js +148 -0
- package/index.ts +44 -0
- package/package.json +64 -0
- package/src/agent-tools.ts +138 -0
- package/src/agents-md.ts +81 -0
- package/src/api-tools.ts +389 -0
- package/src/approvals.ts +129 -0
- package/src/arrange.ts +42 -0
- package/src/catalog.ts +321 -0
- package/src/completions.ts +652 -0
- package/src/confine.ts +350 -0
- package/src/container.ts +220 -0
- package/src/context-tools.ts +435 -0
- package/src/deploy.ts +458 -0
- package/src/egress.ts +176 -0
- package/src/evals.ts +812 -0
- package/src/flow-lint.ts +215 -0
- package/src/git-hook.ts +73 -0
- package/src/git.ts +117 -0
- package/src/gitrepo.ts +391 -0
- package/src/history.ts +329 -0
- package/src/kinds.ts +507 -0
- package/src/library.ts +419 -0
- package/src/linkable.ts +123 -0
- package/src/model-probe.ts +112 -0
- package/src/notify.ts +281 -0
- package/src/oauth-presets.ts +54 -0
- package/src/okf.ts +768 -0
- package/src/openapi.ts +383 -0
- package/src/paths.ts +79 -0
- package/src/platform.ts +87 -0
- package/src/providers.ts +147 -0
- package/src/refs.ts +83 -0
- package/src/run-container.ts +883 -0
- package/src/runner.ts +3446 -0
- package/src/runtime.ts +404 -0
- package/src/script-tools.ts +383 -0
- package/src/secret-files.ts +101 -0
- package/src/secrets.ts +799 -0
- package/src/starter.ts +415 -0
- package/src/step-exec.ts +498 -0
- package/src/storage.ts +771 -0
- package/src/store.ts +2985 -0
- package/src/tar.ts +124 -0
- package/src/tool-names.ts +91 -0
- package/src/tool-programs.ts +65 -0
- package/src/tool-test.ts +401 -0
- package/src/translator.ts +1029 -0
- package/src/triggers.ts +287 -0
- package/src/webhook.ts +183 -0
- package/templates/blog-desk/AGENTS.md +13 -0
- package/templates/blog-desk/agents/researcher/agent.md +11 -0
- package/templates/blog-desk/agents/writer/agent.md +8 -0
- package/templates/blog-desk/evals/writer-quality.md +10 -0
- package/templates/blog-desk/flows/publish.md +10 -0
- package/templates/blog-desk/knowledge/house-style.md +9 -0
- package/templates/blog-desk/knowledge/index.md +9 -0
- package/templates/hello/AGENTS.md +18 -0
- package/templates/hello/agents/curator/agent.md +32 -0
- package/templates/hello/agents/notetaker/agent.md +24 -0
- package/templates/hello/evals/house-style.md +16 -0
- package/templates/hello/flows/memory-review.md +7 -0
- package/templates/hello/flows/note.md +7 -0
- package/templates/hello/knowledge/house-style.md +15 -0
- package/templates/hello/knowledge/index.md +9 -0
- package/templates/hello/skills/plain-english/SKILL.md +11 -0
- package/templates/hello/state/publishing.md +7 -0
- package/templates/hello/tools/wordcount/run.py +13 -0
- package/templates/hello/tools/wordcount/tool.md +22 -0
package/src/providers.ts
ADDED
|
@@ -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
|
+
}
|