@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
package/src/kinds.ts ADDED
@@ -0,0 +1,507 @@
1
+ // Every kind of document foldrun knows about, in one place.
2
+ //
3
+ // The naming, settled: these are **documents**. Not "resources" — MCP already
4
+ // owns that word for something specific, and we speak MCP, so a "resource" in
5
+ // our docs would be genuinely ambiguous. Not "entities" or "assets" either;
6
+ // both are ORM words for things that are, plainly, markdown files. OKF calls
7
+ // them documents and gives every one a `type:`, and we adopted OKF — so the
8
+ // spec says *document*, the UI says the actual noun (agent, flow, skill), and
9
+ // "resource" stays reserved for MCP.
10
+ //
11
+ // That leaves one rule, which this file exists to enforce:
12
+ //
13
+ // **A document's kind is its path. Only OKF documents declare anything.**
14
+ //
15
+ // Agents, flows, evals, skills and tools carry no field naming what they are.
16
+ // The directory already says it — `agents/x/agent.md` is an agent — and every
17
+ // reader here resolves by path: listAgents scans agents/, parseFlow is handed
18
+ // files from flows/. A field restating that is derived data sitting next to
19
+ // its source, free to disagree with it, and nothing consumed it: the runtime
20
+ // never branched on it and the model never saw it, because a system prompt is
21
+ // built from the body, not the frontmatter.
22
+ //
23
+ // `type:` is different and stays. It is OKF's one required field, an open
24
+ // vocabulary saying what a piece of knowledge is *about*, and it is genuinely
25
+ // not derivable — two files side by side in knowledge/ can be a Policy and a
26
+ // Price List. It also does real work: buildIndex groups index.md by it.
27
+ //
28
+ // So the asymmetry is not an oversight. Ours was redundant; OKF's carries
29
+ // information nothing else has.
30
+ //
31
+ // Tools declare `transport:` (http | script | mcp) — how the tool reaches the
32
+ // thing it calls, which is a different question from what the file is. A
33
+ // legacy `type: http` is still read as a transport; see readTransport below.
34
+ //
35
+ // Everything downstream — creation templates, `foldrun init`, the scaffolder,
36
+ // the dashboard's New button — reads this table. Adding a kind means adding a
37
+ // row here and nothing else; that is the point.
38
+
39
+ /** Where a document can live. Nearest wins at read time. */
40
+ export type Scope = "account" | "workspace" | "agent";
41
+
42
+ export type Kind =
43
+ | "agents"
44
+ | "flows"
45
+ | "evals"
46
+ | "tools"
47
+ | "skills"
48
+ | "memory"
49
+ | "knowledge"
50
+ | "scripts";
51
+ // The order that matters is the KINDS table's key order below, not this
52
+ // union's: ALL_KINDS reads the object, and every noun list in the product
53
+ // follows ALL_KINDS. It runs build → do → know → scripts, which is the
54
+ // question each answers rather than the alphabet, and a consistency test
55
+ // holds the sidebar and the asset pages to it.
56
+
57
+ export interface KindMeta {
58
+ kind: Kind;
59
+ /** Singular noun, for buttons and prose. */
60
+ one: string;
61
+ /**
62
+ * The OKF concept type a new document of this kind starts with, for the two
63
+ * kinds that are OKF bundles. Null everywhere else: a kind is read from the
64
+ * path, so nothing else declares what it is.
65
+ *
66
+ * Only a starting point even where it is set — the vocabulary is open.
67
+ */
68
+ docType: string | null;
69
+ /** The frontmatter key carrying `docType`. Always OKF's `type`, or null. */
70
+ docKey: "type" | null;
71
+ /** Scopes this kind can live at, nearest last. */
72
+ scopes: Scope[];
73
+ /** Shown in the name box. */
74
+ placeholder: string;
75
+ /** One line under the name box saying what you're about to make. */
76
+ hint: string;
77
+ /** Path relative to the scope root. */
78
+ file: (name: string) => string;
79
+ /** Starting content. */
80
+ template: (name: string, ctx?: TemplateContext) => string;
81
+ }
82
+
83
+ /** What the caller knows that the template can use. Only flows need it — a new
84
+ * flow that names a real agent is one edit closer to running than one that
85
+ * names a placeholder. */
86
+ export interface TemplateContext {
87
+ firstAgent?: string;
88
+ }
89
+
90
+ const envName = (name: string) => name.toUpperCase().replace(/[^A-Z0-9]+/g, "_");
91
+
92
+ /**
93
+ * A slug as a starting display label: `house-style` → `House style`.
94
+ *
95
+ * A placeholder, not a guess to keep — the author renames it to whatever the
96
+ * document is actually called. Its job is to make the field present, because
97
+ * an absent `title` is the case that silently falls back to the slug.
98
+ */
99
+ const titleCase = (slug: string) => {
100
+ const words = slug.replace(/[-_]+/g, " ").trim();
101
+ return words ? words[0].toUpperCase() + words.slice(1) : slug;
102
+ };
103
+
104
+ export const KINDS: Record<Kind, KindMeta> = {
105
+ agents: {
106
+ kind: "agents",
107
+ one: "agent",
108
+ docType: null,
109
+ docKey: null,
110
+ scopes: ["workspace"],
111
+ placeholder: "researcher",
112
+ hint: "Someone who does one job. Its instructions are the body of the file.",
113
+ file: (name) => `agents/${name}/agent.md`,
114
+ template: (name) => `---
115
+ name: ${name}
116
+ description: What this agent is for, in one line.
117
+ model: fast
118
+ effort: high
119
+ tools: [files]
120
+ ---
121
+
122
+ # ${name}
123
+
124
+ What you do, in plain English. Be specific about what "done" looks like —
125
+ this text is the whole of the agent's instructions.
126
+ `,
127
+ },
128
+
129
+ evals: {
130
+ kind: "evals",
131
+ one: "eval",
132
+ docType: null,
133
+ docKey: null,
134
+ scopes: ["workspace"],
135
+ placeholder: "researcher-quality",
136
+ hint: "Cases an agent must pass. Run them after every edit to its prompt.",
137
+ file: (name) => `evals/${name}.md`,
138
+ template: (name) => `---
139
+ name: ${name}
140
+ agent: agent-name
141
+ description: What this eval protects against.
142
+ ---
143
+
144
+ ## a short name for the case
145
+ task: What to ask the agent.
146
+ expect:
147
+ - contains: something the answer must mention
148
+ - judge: a sentence describing what a good answer does
149
+ `,
150
+ },
151
+
152
+ flows: {
153
+ kind: "flows",
154
+ one: "flow",
155
+ docType: null,
156
+ docKey: null,
157
+ scopes: ["workspace"],
158
+ placeholder: "publish",
159
+ hint: "An ordered list of agents. Same number means they run in parallel.",
160
+ file: (name) => `flows/${name}.md`,
161
+ template: (name, ctx) => `---
162
+ name: ${name}
163
+ description: What this flow accomplishes, end to end.
164
+ trigger: manual
165
+ ---
166
+
167
+ 1. [[${ctx?.firstAgent ?? "agent-name"}]] — what this step does
168
+ `,
169
+ },
170
+
171
+ tools: {
172
+ kind: "tools",
173
+ one: "tool",
174
+ docType: null,
175
+ docKey: null,
176
+ scopes: ["account", "workspace"],
177
+ placeholder: "uuid-service",
178
+ hint: "Something an agent can call. Pick a type, name it, and the files are written for you.",
179
+ file: (name) => `tools/${name}.md`,
180
+ template: (name) => `---
181
+ transport: http
182
+ name: ${name}
183
+ description: What this does, and when to call it.
184
+ base: https://api.example.com/v1
185
+ methods: [GET]
186
+ headers:
187
+ Authorization: Bearer \${${envName(name)}_TOKEN}
188
+ ---
189
+
190
+ Notes for whoever maintains this. Agents opt in with:
191
+
192
+ \`\`\`yaml
193
+ tools: [${name}]
194
+ \`\`\`
195
+ `,
196
+ },
197
+ // Last, and not navigable: a folder tool holds its own code, so scripts
198
+ // is material a tool carries rather than a shelf anyone picks from. The
199
+ // directory still exists and library/x.py still resolves — it just isn't
200
+ // a decision, so the sidebar has no door for it.
201
+ knowledge: {
202
+ kind: "knowledge",
203
+ one: "knowledge doc",
204
+ docType: "Reference",
205
+ // OKF's field and OKF's vocabulary — `Reference` is only the starting point.
206
+ docKey: "type",
207
+ scopes: ["account", "workspace", "agent"],
208
+ placeholder: "pricing",
209
+ hint: "Reference material, given not learned. Agents read it, never write it.",
210
+ file: (name) => `knowledge/${name}.md`,
211
+ // See the memory template: OKF's fields only.
212
+ template: (name) => `---
213
+ type: Reference
214
+ title: ${titleCase(name)}
215
+ description: Reference material agents look up. Given, not learned.
216
+ status: stable
217
+ ---
218
+
219
+ The reference content. Agents read this and never rewrite it.
220
+ `,
221
+ },
222
+
223
+ memory: {
224
+ kind: "memory",
225
+ one: "memory",
226
+ docType: "Fact",
227
+ // OKF's field and OKF's vocabulary — `Fact` is only the starting point.
228
+ docKey: "type",
229
+ scopes: ["account", "workspace", "agent"],
230
+ placeholder: "house-tone",
231
+ hint: "One durable fact, learned. Agents write here themselves.",
232
+ file: (name) => `memory/${name}.md`,
233
+ // `title` and nothing of ours. A bundle carries the format's fields, so a
234
+ // reader that has never heard of this platform loses nothing: `name` was
235
+ // ours, undefined by OKF, and an outside consumer had no reason to read
236
+ // it — it fell back to the filename and showed a slug.
237
+ template: (name) => `---
238
+ type: Fact
239
+ title: ${titleCase(name)}
240
+ description: One durable fact.
241
+ status: stable
242
+ ---
243
+
244
+ The fact.
245
+
246
+ **Why:** what made it worth remembering.
247
+ `,
248
+ },
249
+
250
+ skills: {
251
+ kind: "skills",
252
+ one: "skill",
253
+ docType: null,
254
+ docKey: null,
255
+ scopes: ["account", "workspace", "agent"],
256
+ placeholder: "plain-english",
257
+ hint: "A procedure an agent loads when it needs it. Folder, not one file.",
258
+ file: (name) => `skills/${name}/SKILL.md`,
259
+ template: (name) => `---
260
+ name: ${name}
261
+ description: What this skill does, and when an agent should use it.
262
+ ---
263
+
264
+ # ${name}
265
+
266
+ Step-by-step instructions. Bundle code in this folder's scripts/ if it needs any.
267
+ `,
268
+ },
269
+
270
+ scripts: {
271
+ kind: "scripts",
272
+ one: "script",
273
+ // Code, not a document — no frontmatter to carry either field. A tool
274
+ // file points at it.
275
+ docType: null,
276
+ docKey: null,
277
+ scopes: ["account", "workspace", "agent"],
278
+ placeholder: "slugify",
279
+ hint: "Code an agent runs through a tool. Deterministic work belongs here.",
280
+ file: (name) => `scripts/${name}.py`,
281
+ template: (name) => `#!/usr/bin/env python3
282
+ """${name}"""
283
+ import argparse
284
+
285
+ p = argparse.ArgumentParser()
286
+ p.add_argument("--input")
287
+ args = p.parse_args()
288
+
289
+ print(args.input)
290
+ `,
291
+ },};
292
+
293
+ /**
294
+ * What a script tool can be written in.
295
+ *
296
+ * Three, not every language the runner can execute. The runner picks an
297
+ * interpreter from the file extension, so `.rb` and the rest keep working the
298
+ * moment someone renames a file — but a picker with nine entries is a quiz,
299
+ * and three covers what people actually reach for. The point of asking at all
300
+ * is that "script tool" silently meaning JavaScript was a thing you had to
301
+ * find out by reading the file it made.
302
+ */
303
+ export const SCRIPT_LANGUAGES = [
304
+ { value: "javascript", label: "JavaScript", ext: ".mjs", hint: "node — the default" },
305
+ { value: "python", label: "Python", ext: ".py", hint: "python3" },
306
+ { value: "bash", label: "Shell", ext: ".sh", hint: "bash — for wrapping a command" },
307
+ ] as const;
308
+
309
+ export type ScriptLanguage = (typeof SCRIPT_LANGUAGES)[number]["value"];
310
+
311
+ /** The starting program, in each language. Every one of them does the same
312
+ * three things, because those three are the tool contract:
313
+ *
314
+ * validate its own arguments — every declared arg is OPTIONAL at the
315
+ * call site, so required means checked here
316
+ * print the result on stdout — that is what the agent reads back
317
+ * exit non-zero on failure — that is what tells the agent it failed
318
+ */
319
+ const PROGRAMS: Record<ScriptLanguage, string> = {
320
+ javascript: `import { parseArgs } from "node:util";
321
+
322
+ // Every declared arg is optional at the call site — the model may omit any of
323
+ // them, and an empty value is never passed — so required means checked here.
324
+ const { values } = parseArgs({ options: { input: { type: "string" } } });
325
+ if (!values.input) {
326
+ console.error("need --input");
327
+ process.exit(1);
328
+ }
329
+
330
+ // One JSON object on stdout: small, structured, and the same shape every call.
331
+ console.log(JSON.stringify({ input: values.input }));
332
+ `,
333
+ python: `#!/usr/bin/env python3
334
+ import argparse, json
335
+
336
+ # Every declared arg is optional at the call site — the model may omit any of
337
+ # them, and an empty value is never passed — so required means checked here.
338
+ # argparse exits 2 with a usage line on stderr, which is exactly the signal
339
+ # the agent should get.
340
+ p = argparse.ArgumentParser()
341
+ p.add_argument("--input", required=True)
342
+ a = p.parse_args()
343
+
344
+ # One JSON object on stdout: small, structured, and the same shape every call.
345
+ print(json.dumps({"input": a.input}))
346
+ `,
347
+ bash: `#!/usr/bin/env bash
348
+ set -euo pipefail
349
+
350
+ # Declared args arrive as long flags: --input value
351
+ input=""
352
+ while [ $# -gt 0 ]; do
353
+ case "$1" in
354
+ --input) input="\${2-}"; shift 2 ;;
355
+ *) echo "unexpected argument: $1" >&2; exit 1 ;;
356
+ esac
357
+ done
358
+
359
+ # Every declared arg is optional at the call site, so required means checked
360
+ # here. A non-zero exit is what tells the agent the call failed.
361
+ [ -n "$input" ] || { echo "need --input" >&2; exit 1; }
362
+
363
+ # Whatever this prints on stdout is what the agent reads back. Keep it small
364
+ # and regular — print JSON when the result has structure.
365
+ echo "$input"
366
+ `,
367
+ };
368
+
369
+ /**
370
+ * A new tool, as the files it starts as.
371
+ *
372
+ * Three transports, two shapes. An API or an MCP server is a definition and
373
+ * nothing else, so it stays one flat file. A script tool is a definition and
374
+ * the code it runs, which cannot be separated without the tool silently
375
+ * resolving to nothing — so it starts as a folder holding both, with `run:`
376
+ * relative to itself and naming no scope.
377
+ *
378
+ * Returned as a list because "create a tool" is not always one write, and
379
+ * the callers that stamp templates should not have to know which case they
380
+ * are in.
381
+ */
382
+ export function toolStarter(
383
+ name: string,
384
+ transport: "http" | "script" | "mcp",
385
+ language: ScriptLanguage = "javascript",
386
+ ): { file: string; content: string }[] {
387
+ if (transport === "script") {
388
+ // A folder, because the program is a file. Code in a fenced block gets
389
+ // no linter, no type checker, no formatter and no way to run it except
390
+ // through a run — and the agents editing these are file-shaped tools:
391
+ // they can patch one line of run.mjs and execute it, where a fence has
392
+ // to be re-emitted whole and can only be tested by shipping it. The
393
+ // single-file form still parses, for a tool small enough to read at a
394
+ // glance; it is no longer what a new one starts as.
395
+ //
396
+ // The language is asked for rather than assumed. "Script tool" used to
397
+ // mean JavaScript silently, which you found out by opening the file it
398
+ // wrote; the runner has always picked an interpreter from the extension,
399
+ // so the choice was already real and merely hidden.
400
+ const lang = SCRIPT_LANGUAGES.find((l) => l.value === language) ?? SCRIPT_LANGUAGES[0];
401
+ const program = `run${lang.ext}`;
402
+ return [
403
+ {
404
+ file: `tools/${name}/tool.md`,
405
+ content: `---
406
+ transport: script
407
+ name: ${name}
408
+ description: What this does, and when an agent should call it.
409
+ run: ${program}
410
+ args:
411
+ input: What the caller passes in
412
+ timeout: 60
413
+ ---
414
+
415
+ \`${program}\` beside this file is the tool. Arguments arrive as --flags, one
416
+ per \`args:\` entry; whatever it prints on stdout is what the agent reads back,
417
+ and a non-zero exit is what tells the agent the call failed.
418
+
419
+ \`run:\` names the file, never the scope it sits in, so this folder is correct
420
+ copied into any workspace or installed at the account.
421
+
422
+ Needs a package? Declare it here and it is installed for every agent that
423
+ grants this tool — the tool is the unit of code, so its dependencies travel
424
+ with it:
425
+
426
+ \`\`\`yaml
427
+ runtime:
428
+ packages: [requests] # pip
429
+ npm: [cheerio] # npm
430
+ \`\`\`
431
+
432
+ Agents opt in with:
433
+
434
+ \`\`\`yaml
435
+ tools: [${name}]
436
+ \`\`\`
437
+
438
+ Run it by hand while you are writing it:
439
+
440
+ \`\`\`console
441
+ ${lang.value === "python" ? "python3" : lang.value === "bash" ? "bash" : "node"} tools/${name}/${program} --input hello
442
+ \`\`\`
443
+ `,
444
+ },
445
+ { file: `tools/${name}/${program}`, content: PROGRAMS[lang.value] },
446
+ ];
447
+ }
448
+
449
+ if (transport === "mcp") {
450
+ return [
451
+ {
452
+ file: `tools/${name}.md`,
453
+ content: `---
454
+ transport: mcp
455
+ name: ${name}
456
+ description: What this server provides, and when an agent should reach for it.
457
+ command: npx
458
+ args: ["-y", "@example/mcp-server"]
459
+ env:
460
+ EXAMPLE_TOKEN: \${${envName(name)}_TOKEN}
461
+ ---
462
+
463
+ Every tool this server exposes reaches the agent that opts in with:
464
+
465
+ \`\`\`yaml
466
+ tools: [${name}]
467
+ \`\`\`
468
+ `,
469
+ },
470
+ ];
471
+ }
472
+
473
+ return [{ file: KINDS.tools.file(name), content: KINDS.tools.template(name) }];
474
+ }
475
+
476
+ export const ALL_KINDS = Object.keys(KINDS) as Kind[];
477
+
478
+ /** The kinds that exist at a given scope. */
479
+ export const kindsAt = (scope: Scope) => ALL_KINDS.filter((k) => KINDS[k].scopes.includes(scope));
480
+
481
+ /** The noun a kind declares, for asserting a file says what it is. */
482
+ export const docTypeOf = (kind: Kind) => KINDS[kind].docType;
483
+
484
+ /** Which frontmatter key carries that noun — `kind:` for ours, `type:` for OKF's. */
485
+ export const docKeyOf = (kind: Kind) => KINDS[kind].docKey;
486
+
487
+
488
+ /**
489
+ * How a tool reaches the thing it calls.
490
+ *
491
+ * Read from `transport:`, falling back to a legacy `type: http|script|mcp`
492
+ * written before `type:` meant the document. Files written either way keep
493
+ * working; only one of them is what we generate now.
494
+ */
495
+ export type Transport = "http" | "script" | "mcp";
496
+
497
+ const TRANSPORTS = new Set<Transport>(["http", "script", "mcp"]);
498
+
499
+ export function readTransport(data: Record<string, unknown>): Transport | null {
500
+ const explicit = typeof data.transport === "string" ? data.transport.toLowerCase() : null;
501
+ if (explicit && TRANSPORTS.has(explicit as Transport)) return explicit as Transport;
502
+
503
+ const legacy = typeof data.type === "string" ? data.type.toLowerCase() : null;
504
+ if (legacy && TRANSPORTS.has(legacy as Transport)) return legacy as Transport;
505
+
506
+ return null;
507
+ }