@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/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
|
+
}
|