@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,108 @@
1
+ import { type Effort, type Tier, type FlowInfo } from "./store.ts";
2
+ import type { FlowWarning } from "./flow-lint.ts";
3
+ export interface CatalogModel {
4
+ id: string;
5
+ name: string;
6
+ contextLength: number | null;
7
+ /** USD per input token, as the gateway quotes it (not per MTok). */
8
+ promptPrice: number | null;
9
+ /** USD per output token. */
10
+ completionPrice: number | null;
11
+ /** Can this model make tool calls? Null when the gateway doesn't say. */
12
+ tools: boolean | null;
13
+ /** Does it take a reasoning-depth parameter? (What `effort:` needs.) */
14
+ reasoning: boolean | null;
15
+ /** The effort levels this model accepts, when the gateway names them —
16
+ * OpenRouter's level names are ours (low…max, plus minimal/none below
17
+ * our scale), so no translation table sits between the two. */
18
+ efforts: string[] | null;
19
+ }
20
+ export interface Catalog {
21
+ /** The base_url this catalogue was fetched from. */
22
+ baseUrl: string;
23
+ fetchedAt: string;
24
+ models: CatalogModel[];
25
+ }
26
+ /** The cache as it stands, however old — null only when nothing was ever
27
+ * fetched. Reading never touches the network. */
28
+ export declare function readCatalog(baseUrl: string): Catalog | null;
29
+ /**
30
+ * The cache, refreshed over the network when it is older than the TTL.
31
+ * A failed fetch keeps the stale copy — offline is a reason to be out of
32
+ * date, not a reason to forget everything — and returns null only when
33
+ * there is nothing at all to fall back on.
34
+ */
35
+ export declare function loadCatalog(baseUrl: string): Promise<Catalog | null>;
36
+ /** A preset reference — settings live in the gateway's dashboard, opaque to
37
+ * us on purpose. The catalogue offers no opinion on one. */
38
+ export declare const isPresetRef: (model: string) => boolean;
39
+ export declare function findModel(catalog: Catalog | null, id: string): CatalogModel | null;
40
+ export interface ModelVerdict {
41
+ ok: boolean;
42
+ /** One line, naming the cause and the nearest fix. Only set when !ok. */
43
+ reason?: string;
44
+ }
45
+ /**
46
+ * The run-start gate. Only ever fails on *knowledge*: an id the catalogue
47
+ * has, whose declared capabilities rule the step out. Unknown ids, preset
48
+ * refs, tier names that resolved elsewhere, and an absent catalogue all
49
+ * pass — the model call itself is the authority of last resort, and a gate
50
+ * that blocks on ignorance would make offline development impossible.
51
+ */
52
+ export declare function checkModel(catalog: Catalog | null, modelId: string, needs: {
53
+ tools: boolean;
54
+ }): ModelVerdict;
55
+ /** True when `effort:` will be silently ignored by this model — worth a
56
+ * trace line, never a failure: the step still runs, just without the depth
57
+ * the file asked for. */
58
+ export declare function effortIgnored(catalog: Catalog | null, modelId: string): boolean;
59
+ /**
60
+ * The effort that will actually reach this model, and the sentence that
61
+ * explains any difference. Three outcomes:
62
+ *
63
+ * - the model takes this level → unchanged, silent
64
+ * - it reasons, but not at this level → nearest supported level, said aloud
65
+ * - it does not reason at all → null, said aloud
66
+ *
67
+ * "Nearest" prefers the highest supported level at or below the request —
68
+ * someone who wrote `max` wanted the ceiling, and the ceiling here is
69
+ * `xhigh` — falling back to the lowest above it. Unknown models and absent
70
+ * catalogues pass the request through untouched: no knowledge, no opinion,
71
+ * same as the gate.
72
+ */
73
+ export declare function clampEffort(catalog: Catalog | null, modelId: string, effort: Effort | null): {
74
+ effort: Effort | null;
75
+ note: string | null;
76
+ };
77
+ /**
78
+ * What one step actually cost through this gateway, or null when the
79
+ * catalogue can't say. The SDK's own costUsd is priced off Anthropic's
80
+ * table, which is wrong for every routed model — so when the catalogue
81
+ * knows better, its number wins.
82
+ */
83
+ export declare function catalogCost(catalog: Catalog | null, modelId: string, usage: {
84
+ inputTokens: number;
85
+ outputTokens: number;
86
+ }): number | null;
87
+ /** Catalogue entries as completion items: price per MTok, context, and the
88
+ * one capability that decides agent-worthiness. Tool-capable models first —
89
+ * on an agent platform they are almost always what the author wants. */
90
+ export declare function catalogCompletions(catalog: Catalog | null): {
91
+ id: string;
92
+ hint: string;
93
+ }[];
94
+ /** What the lint needs to know about each agent: its own model, and whether
95
+ * it declares any tools at all. */
96
+ export interface AgentModelInfo {
97
+ name: string;
98
+ model: string;
99
+ usesTools: boolean;
100
+ }
101
+ /**
102
+ * The run-start gate's veto, raised while the file is still open. Same
103
+ * knowledge, earlier moment: a step whose resolved model the catalogue says
104
+ * cannot call tools — run by an agent that declares some — gets a warning in
105
+ * the editor instead of a failed step tomorrow morning. Advisory like every
106
+ * flow lint; the run-start gate stays the enforcement.
107
+ */
108
+ export declare function lintFlowModels(flow: FlowInfo, agents: AgentModelInfo[], catalog: Catalog | null, providerModels?: Partial<Record<Tier, string>>): FlowWarning[];
@@ -0,0 +1,255 @@
1
+ // The model catalogue: what a gateway can actually run, cached.
2
+ //
3
+ // "Any model they want" is only an offer if picking a wrong one fails in one
4
+ // line at step start — not three minutes in, as an agent that narrates tool
5
+ // calls into prose because its model cannot make them. OpenRouter's models
6
+ // endpoint declares, per model, which parameters it supports (`tools` being
7
+ // the one that decides whether an agent loop is possible at all), plus
8
+ // context length and real prices. This module fetches that once, caches it
9
+ // on disk, and answers three questions from the cache:
10
+ //
11
+ // - can this model drive tools? → the run-start gate
12
+ // - what models exist, at what price? → `model:` autocomplete
13
+ // - what does a token actually cost? → metering for routed models
14
+ //
15
+ // Everything degrades to "don't know": a missing cache, an offline fetch or
16
+ // an unknown id yields null, and callers treat null as "no opinion" — the
17
+ // gate never blocks on ignorance, only on knowledge. A preset reference
18
+ // (`@preset/…`) is opaque by design: its settings live in the provider's
19
+ // dashboard, so the catalogue has no opinion about it either.
20
+ import fs from "node:fs";
21
+ import path from "node:path";
22
+ import { dataRoot } from "./paths.js";
23
+ import { resolveModel, resolveTier, MODEL_TIERS, EFFORT_LEVELS } from "./store.js";
24
+ /** A day. Model lists move weekly, not hourly, and a stale price in an
25
+ * autocomplete hint is a smaller failure than a fetch on every keystroke. */
26
+ const TTL_MS = 24 * 60 * 60 * 1000;
27
+ const cacheFile = (baseUrl) => path.join(dataRoot(), "catalog",
28
+ // One file per endpoint, named legibly: catalog/openrouter.ai.json.
29
+ `${new URL(baseUrl).hostname.replace(/[^a-z0-9.-]/gi, "_")}.json`);
30
+ function parseModels(payload) {
31
+ const data = payload?.data;
32
+ if (!Array.isArray(data))
33
+ return [];
34
+ const out = [];
35
+ for (const raw of data) {
36
+ if (!raw || typeof raw !== "object")
37
+ continue;
38
+ const m = raw;
39
+ if (typeof m.id !== "string")
40
+ continue;
41
+ const supported = Array.isArray(m.supported_parameters)
42
+ ? m.supported_parameters.map(String)
43
+ : null;
44
+ const pricing = (m.pricing ?? {});
45
+ const price = (k) => {
46
+ const n = Number(pricing[k]);
47
+ return Number.isFinite(n) ? n : null;
48
+ };
49
+ out.push({
50
+ id: m.id,
51
+ name: typeof m.name === "string" ? m.name : m.id,
52
+ contextLength: Number.isFinite(Number(m.context_length)) ? Number(m.context_length) : null,
53
+ promptPrice: price("prompt"),
54
+ completionPrice: price("completion"),
55
+ tools: supported ? supported.includes("tools") : null,
56
+ reasoning: supported ? supported.includes("reasoning") : null,
57
+ efforts: Array.isArray(m.reasoning?.supported_efforts)
58
+ ? m.reasoning.supported_efforts.map(String)
59
+ : null,
60
+ });
61
+ }
62
+ return out;
63
+ }
64
+ /** The cache as it stands, however old — null only when nothing was ever
65
+ * fetched. Reading never touches the network. */
66
+ export function readCatalog(baseUrl) {
67
+ try {
68
+ const parsed = JSON.parse(fs.readFileSync(cacheFile(baseUrl), "utf8"));
69
+ return Array.isArray(parsed.models) ? parsed : null;
70
+ }
71
+ catch {
72
+ return null;
73
+ }
74
+ }
75
+ /**
76
+ * The cache, refreshed over the network when it is older than the TTL.
77
+ * A failed fetch keeps the stale copy — offline is a reason to be out of
78
+ * date, not a reason to forget everything — and returns null only when
79
+ * there is nothing at all to fall back on.
80
+ */
81
+ export async function loadCatalog(baseUrl) {
82
+ const cached = readCatalog(baseUrl);
83
+ if (cached && Date.now() - Date.parse(cached.fetchedAt) < TTL_MS)
84
+ return cached;
85
+ try {
86
+ const res = await fetch(`${baseUrl.replace(/\/+$/, "")}/v1/models`, {
87
+ signal: AbortSignal.timeout(10_000),
88
+ });
89
+ if (!res.ok)
90
+ throw new Error(`${res.status}`);
91
+ const models = parseModels(await res.json());
92
+ if (!models.length)
93
+ throw new Error("empty model list");
94
+ const catalog = { baseUrl, fetchedAt: new Date().toISOString(), models };
95
+ fs.mkdirSync(path.dirname(cacheFile(baseUrl)), { recursive: true });
96
+ // Atomic like every other store write: a crashed refresh must not leave
97
+ // half a JSON file where the whole catalogue used to be.
98
+ const tmp = cacheFile(baseUrl) + ".tmp";
99
+ fs.writeFileSync(tmp, JSON.stringify(catalog));
100
+ fs.renameSync(tmp, cacheFile(baseUrl));
101
+ return catalog;
102
+ }
103
+ catch {
104
+ return cached; // stale beats nothing; null when there is no nothing
105
+ }
106
+ }
107
+ /** A preset reference — settings live in the gateway's dashboard, opaque to
108
+ * us on purpose. The catalogue offers no opinion on one. */
109
+ export const isPresetRef = (model) => model.startsWith("@preset/");
110
+ export function findModel(catalog, id) {
111
+ if (!catalog || isPresetRef(id))
112
+ return null;
113
+ return catalog.models.find((m) => m.id === id) ?? null;
114
+ }
115
+ /**
116
+ * The run-start gate. Only ever fails on *knowledge*: an id the catalogue
117
+ * has, whose declared capabilities rule the step out. Unknown ids, preset
118
+ * refs, tier names that resolved elsewhere, and an absent catalogue all
119
+ * pass — the model call itself is the authority of last resort, and a gate
120
+ * that blocks on ignorance would make offline development impossible.
121
+ */
122
+ export function checkModel(catalog, modelId, needs) {
123
+ const m = findModel(catalog, modelId);
124
+ if (!m)
125
+ return { ok: true };
126
+ if (needs.tools && m.tools === false) {
127
+ const alternative = nearestWithTools(catalog, m);
128
+ return {
129
+ ok: false,
130
+ reason: `model ${m.id} cannot call tools, and this agent declares some — the run would ` +
131
+ `narrate tool use as text instead of doing it.` +
132
+ (alternative ? ` Nearest model that can: ${alternative.id}.` : ""),
133
+ };
134
+ }
135
+ return { ok: true };
136
+ }
137
+ /** Same family first (everything before the slash), then anything with
138
+ * tools — "use the provider's sibling" is advice people actually take. */
139
+ function nearestWithTools(catalog, from) {
140
+ const vendor = from.id.split("/")[0];
141
+ const candidates = catalog.models.filter((m) => m.tools === true);
142
+ return candidates.find((m) => m.id.startsWith(vendor + "/")) ?? candidates[0] ?? null;
143
+ }
144
+ /** True when `effort:` will be silently ignored by this model — worth a
145
+ * trace line, never a failure: the step still runs, just without the depth
146
+ * the file asked for. */
147
+ export function effortIgnored(catalog, modelId) {
148
+ const m = findModel(catalog, modelId);
149
+ return m !== null && m.reasoning === false;
150
+ }
151
+ /**
152
+ * The effort that will actually reach this model, and the sentence that
153
+ * explains any difference. Three outcomes:
154
+ *
155
+ * - the model takes this level → unchanged, silent
156
+ * - it reasons, but not at this level → nearest supported level, said aloud
157
+ * - it does not reason at all → null, said aloud
158
+ *
159
+ * "Nearest" prefers the highest supported level at or below the request —
160
+ * someone who wrote `max` wanted the ceiling, and the ceiling here is
161
+ * `xhigh` — falling back to the lowest above it. Unknown models and absent
162
+ * catalogues pass the request through untouched: no knowledge, no opinion,
163
+ * same as the gate.
164
+ */
165
+ export function clampEffort(catalog, modelId, effort) {
166
+ if (!effort)
167
+ return { effort: null, note: null };
168
+ const m = findModel(catalog, modelId);
169
+ if (!m)
170
+ return { effort, note: null };
171
+ if (m.reasoning === false && !m.efforts?.length) {
172
+ return {
173
+ effort: null,
174
+ note: `model ${m.id} takes no reasoning parameter — effort: ${effort} will have no effect`,
175
+ };
176
+ }
177
+ if (!m.efforts?.length)
178
+ return { effort, note: null }; // reasons, levels unnamed
179
+ const supported = EFFORT_LEVELS.filter((l) => m.efforts.includes(l));
180
+ if (!supported.length || supported.includes(effort))
181
+ return { effort, note: null };
182
+ const want = EFFORT_LEVELS.indexOf(effort);
183
+ const below = [...supported].reverse().find((l) => EFFORT_LEVELS.indexOf(l) < want);
184
+ const clamped = below ?? supported[0];
185
+ return {
186
+ effort: clamped,
187
+ note: `model ${m.id} supports effort ${supported.join(", ")} — ` +
188
+ `effort: ${effort} runs as ${clamped}`,
189
+ };
190
+ }
191
+ /**
192
+ * What one step actually cost through this gateway, or null when the
193
+ * catalogue can't say. The SDK's own costUsd is priced off Anthropic's
194
+ * table, which is wrong for every routed model — so when the catalogue
195
+ * knows better, its number wins.
196
+ */
197
+ export function catalogCost(catalog, modelId, usage) {
198
+ const m = findModel(catalog, modelId);
199
+ if (!m || m.promptPrice == null || m.completionPrice == null)
200
+ return null;
201
+ return usage.inputTokens * m.promptPrice + usage.outputTokens * m.completionPrice;
202
+ }
203
+ /** Catalogue entries as completion items: price per MTok, context, and the
204
+ * one capability that decides agent-worthiness. Tool-capable models first —
205
+ * on an agent platform they are almost always what the author wants. */
206
+ export function catalogCompletions(catalog) {
207
+ if (!catalog)
208
+ return [];
209
+ const perMTok = (v) => (v == null ? "?" : `$${(v * 1_000_000).toFixed(2)}`);
210
+ const ctx = (n) => (n == null ? "" : n >= 1000 ? ` · ${Math.round(n / 1000)}k ctx` : ` · ${n} ctx`);
211
+ return [...catalog.models]
212
+ .sort((a, b) => Number(b.tools === true) - Number(a.tools === true) || a.id.localeCompare(b.id))
213
+ .map((m) => ({
214
+ id: m.id,
215
+ hint: `${perMTok(m.promptPrice)}/${perMTok(m.completionPrice)} MTok${ctx(m.contextLength)}${m.tools === false ? " · no tools" : ""}`,
216
+ }));
217
+ }
218
+ /**
219
+ * The run-start gate's veto, raised while the file is still open. Same
220
+ * knowledge, earlier moment: a step whose resolved model the catalogue says
221
+ * cannot call tools — run by an agent that declares some — gets a warning in
222
+ * the editor instead of a failed step tomorrow morning. Advisory like every
223
+ * flow lint; the run-start gate stays the enforcement.
224
+ */
225
+ export function lintFlowModels(flow, agents, catalog, providerModels = {}) {
226
+ if (!catalog)
227
+ return [];
228
+ const warnings = [];
229
+ const byName = new Map(agents.map((a) => [a.name, a]));
230
+ // The same remap the runner applies on the wire: a tier the provider
231
+ // block renames is checked as the gateway's id.
232
+ const wire = (value) => {
233
+ const resolved = resolveModel(value);
234
+ const tier = Object.entries(MODEL_TIERS).find(([, id]) => id === resolved)?.[0] ?? resolveTier(value);
235
+ return (tier && providerModels[tier]) || resolved;
236
+ };
237
+ flow.steps.forEach((step, i) => {
238
+ if (step.subflow)
239
+ return; // the subflow's own lint covers its steps
240
+ const agent = byName.get(step.agent);
241
+ if (!agent?.usesTools)
242
+ return;
243
+ const model = wire(step.model ?? flow.model ?? agent.model);
244
+ const verdict = checkModel(catalog, model, { tools: true });
245
+ if (!verdict.ok) {
246
+ warnings.push({
247
+ step: i,
248
+ line: step.line,
249
+ message: `"${step.agent}" would run on ${model}, which cannot call tools`,
250
+ detail: verdict.reason,
251
+ });
252
+ }
253
+ });
254
+ return warnings;
255
+ }
@@ -0,0 +1,61 @@
1
+ export interface Vocabulary {
2
+ agents: string[];
3
+ flows: string[];
4
+ skills: string[];
5
+ tools: string[];
6
+ secrets: string[];
7
+ scripts: string[];
8
+ /** Everything [[ ]] can point at besides agents and flows: knowledge and
9
+ * memory docs, and paths in the file store. One reference syntax for the
10
+ * whole workspace — the Obsidian instinct, honoured. */
11
+ docs?: {
12
+ name: string;
13
+ hint: string;
14
+ }[];
15
+ /** OKF `type:` values already used here. The index groups by exact string,
16
+ * so offering what exists is what keeps a bundle consistent. */
17
+ types: string[];
18
+ /** The gateway's catalogue, when this workspace declares a provider —
19
+ * full ids with price and capability in the hint. Tiers stay first:
20
+ * they are the portable answer, the catalogue is the informed one. */
21
+ models?: {
22
+ id: string;
23
+ hint: string;
24
+ }[];
25
+ }
26
+ export interface Completion {
27
+ label: string;
28
+ /** Text to insert in place of the query. Defaults to label. */
29
+ insert?: string;
30
+ hint?: string;
31
+ }
32
+ export interface CompletionContext {
33
+ /** Character offset where the replacement starts. */
34
+ from: number;
35
+ query: string;
36
+ items: Completion[];
37
+ /** Shown above the list, e.g. "HTTP method". */
38
+ title: string;
39
+ }
40
+ declare const FIELDS: Record<string, Completion[]>;
41
+ export type FileKind = keyof typeof FIELDS;
42
+ export declare function fileKind(path: string): FileKind;
43
+ /**
44
+ * Does [[ ]] in this file get expanded into a path, or stay literal?
45
+ *
46
+ * The rule is not "which directory" but "does this text become a prompt".
47
+ * An agent's body, a flow step's instruction and an eval's task all end up
48
+ * in front of a model, which cannot open `[[house-style]]` but can open
49
+ * `../../knowledge/house-style.md`. A knowledge or tool document is read as
50
+ * a file, and its links are for the person reading it.
51
+ *
52
+ * Exported so the editor can say which case a file is in, rather than
53
+ * offering the same word for two different behaviours.
54
+ */
55
+ export declare function resolvesLinks(path: string): boolean;
56
+ /**
57
+ * What can go at the cursor?
58
+ * @returns null when nothing sensible can be suggested.
59
+ */
60
+ export declare function completionsAt(path: string, text: string, cursor: number, vocab: Vocabulary): CompletionContext | null;
61
+ export {};