@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,252 @@
1
+ // Telling a person what their agents did while they were not looking.
2
+ //
3
+ // An agent that runs when the laptop is closed needs a way to say "I
4
+ // finished", "I failed", and above all "I am waiting for you" — a parked
5
+ // approval nobody hears about is a run that never happens. The channel is a
6
+ // webhook out: one URL, declared in AGENTS.md frontmatter, POSTed a small
7
+ // JSON body. Slack, Discord, ntfy, a phone-push relay and a plain inbox
8
+ // service all speak that; building any one of them natively would just be
9
+ // this plus vendor formatting.
10
+ //
11
+ // notify:
12
+ // url: ${SLACK_WEBHOOK_URL} # a secret name, or a literal URL
13
+ // events: [failed, awaiting-approval, completed]
14
+ //
15
+ // Or, for people who live in an inbox rather than a channel:
16
+ //
17
+ // notify:
18
+ // email: ops@example.com # sent via the RESEND_API_KEY connection
19
+ // events: [failed, completed]
20
+ //
21
+ // Email is the one vendor worth speaking natively: the platform already
22
+ // holds a Resend connection for its email tool, and "just email me" is the
23
+ // notification default for most of the world. Everything else stays a
24
+ // webhook — one URL covers Slack, Discord, ntfy and every relay.
25
+ //
26
+ // Workspace config replaces account config whole, like provider: — a URL is
27
+ // a destination, and merging two destinations sends half your alerts to a
28
+ // channel you stopped reading.
29
+ //
30
+ // Defaults: failed and awaiting-approval. Completed is opt-in — a schedule
31
+ // that works is the quiet kind of good news, and a channel that pings on
32
+ // every success gets muted, which un-pings the failures too.
33
+ import { readAgentsMd } from "./runner.js";
34
+ import { accountDir, workspaceDir, runCost } from "./store.js";
35
+ import { getSecret } from "./secrets.js";
36
+ import { approveToken, publicUrl } from "./webhook.js";
37
+ /**
38
+ * The platform's own mail: an invite, a low balance.
39
+ *
40
+ * These are from foldrun, about the platform, to the account's owner — so
41
+ * they go through the platform's Resend key and sender (FOLDRUN_RESEND_API_KEY,
42
+ * FOLDRUN_EMAIL_FROM), never through anything the customer configured.
43
+ *
44
+ * Without a platform key — the CLI on a laptop, a test — the account's own
45
+ * key and EMAIL_FROM are the fallback.
46
+ */
47
+ export function platformMail(tenant) {
48
+ const key = process.env.FOLDRUN_RESEND_API_KEY;
49
+ if (key)
50
+ return { key, from: process.env.FOLDRUN_EMAIL_FROM || "foldrun <hello@foldrun.io>" };
51
+ return accountMail(tenant);
52
+ }
53
+ /** The account's own Resend key and sender, or null when it has none. */
54
+ export function accountMail(tenant) {
55
+ const own = getSecret(tenant, "RESEND_API_KEY");
56
+ if (!own)
57
+ return null;
58
+ const from = getSecret(tenant, "EMAIL_FROM")?.value?.trim() || "foldrun <onboarding@resend.dev>";
59
+ return { key: own.value, from };
60
+ }
61
+ /**
62
+ * A run notification is the account's mail, not the platform's: it is a
63
+ * desk telling its owner what it found, and the owner chose the sender —
64
+ * their RESEND_API_KEY and EMAIL_FROM on the account, the same connection
65
+ * their agents' `email` tool uses. So it comes from the address they
66
+ * expect, under their own domain's reputation and their own inbox rules;
67
+ * 2026-09-06, every notification from the platform's address was in the
68
+ * owner's bin. The platform's key is the fallback for an account that set
69
+ * none, so `notify:` works before anyone has configured mail at all.
70
+ */
71
+ export function notifyMail(tenant) {
72
+ return accountMail(tenant) ?? platformMail(tenant);
73
+ }
74
+ const DEFAULT_EVENTS = ["failed", "awaiting-approval"];
75
+ export function notifyConfig(tenant, workspace) {
76
+ const raw = readAgentsMd(workspaceDir(tenant, workspace))?.data.notify ??
77
+ readAgentsMd(accountDir(tenant))?.data.notify;
78
+ if (!raw)
79
+ return null;
80
+ if (typeof raw === "string") {
81
+ // A bare string is whichever destination it looks like.
82
+ return raw.includes("@") && !raw.includes("/")
83
+ ? { email: raw, events: DEFAULT_EVENTS }
84
+ : { url: raw, events: DEFAULT_EVENTS };
85
+ }
86
+ if (typeof raw === "object" && raw !== null) {
87
+ const o = raw;
88
+ const url = typeof o.url === "string" ? o.url : undefined;
89
+ const email = typeof o.email === "string" ? o.email : undefined;
90
+ if (!url && !email)
91
+ return null;
92
+ return {
93
+ url,
94
+ email,
95
+ events: Array.isArray(o.events) && o.events.length ? o.events.map(String) : DEFAULT_EVENTS,
96
+ };
97
+ }
98
+ return null;
99
+ }
100
+ // Said once per process, not once per run: the fix is one env edit, and a
101
+ // line per parked run would bury the failures around it.
102
+ let warnedNoPublicUrl = false;
103
+ /**
104
+ * The approve/reject links for a run waiting on a person, or null when the
105
+ * install cannot say where it lives. A notification that only says "waiting
106
+ * for you" sends the reader to find a laptop; one that carries the decision
107
+ * lets them make it where they read it.
108
+ *
109
+ * The reject link goes to the same page with the choice pre-selected — a
110
+ * GET must not decide anything, because inbox link-checkers follow links.
111
+ */
112
+ export function approvalLinks(tenant, workspace, runId) {
113
+ const base = publicUrl();
114
+ if (!base) {
115
+ if (!warnedNoPublicUrl) {
116
+ warnedNoPublicUrl = true;
117
+ console.error("[foldrun] notify: approval links need FOLDRUN_PUBLIC_URL (the install's public origin); sending without them");
118
+ }
119
+ return null;
120
+ }
121
+ const approveUrl = `${base}/api/approve/${tenant}/${workspace}/${runId}?token=${approveToken(tenant, workspace, runId)}`;
122
+ return { approveUrl, rejectUrl: `${approveUrl}&decision=reject` };
123
+ }
124
+ /**
125
+ * Fire the webhook for a run's state, if the workspace asked to hear about
126
+ * it. Failures are logged and swallowed — a broken Slack hook must never
127
+ * fail the run it is reporting on.
128
+ */
129
+ /** Flows whose completion is nobody's news: eval cases and adhoc runs. */
130
+ export function isQuietFlow(flow) {
131
+ return /^(eval|adhoc):/.test(flow);
132
+ }
133
+ export async function sendRunNotification(tenant, workspace, run) {
134
+ const config = notifyConfig(tenant, workspace);
135
+ if (!config || !config.events.includes(run.status))
136
+ return false;
137
+ // A test is not news. An eval case or an adhoc single-agent run is started
138
+ // by a person who is watching it, and a desk that emails "completed" for
139
+ // every one of them buries the weekly verdict under two hundred of these —
140
+ // on 2026-09-06 the reader's inbox rule had sent the lot to the bin, the
141
+ // real reports with them. Failures and gates still send: a person asked.
142
+ if (run.status === "completed" && isQuietFlow(run.flow))
143
+ return false;
144
+ const failed = run.steps.filter((s) => s.status === "failed").map((s) => s.agent);
145
+ const waitingSteps = run.steps.filter((s) => s.status === "awaiting-approval");
146
+ const waiting = waitingSteps.map((s) => s.agent);
147
+ // A park on `wait: event` is not a question for the reader: the message
148
+ // says what the run is waiting for, so nobody hunts for a button that
149
+ // the outside world is meant to press.
150
+ const onEvent = waitingSteps.length > 0 && waitingSteps.every((s) => s.waitFor === "event");
151
+ const headline = run.status === "completed"
152
+ ? `✓ ${run.flow} completed`
153
+ : run.status === "failed"
154
+ ? `✗ ${run.flow} failed${failed.length ? ` at ${failed.join(", ")}` : ""}`
155
+ : onEvent
156
+ ? `⏳ ${run.flow} is waiting for an external event${waiting.length ? ` (${waiting.join(", ")})` : ""}`
157
+ : `⏸ ${run.flow} is waiting for your approval${waiting.length ? ` (${waiting.join(", ")})` : ""}`;
158
+ // What the run concluded, if it concluded anything. This is the whole point
159
+ // of the message: "✓ health completed · $0.42" tells you a thing ran and
160
+ // what it cost, and nothing at all about what it found. A scheduled desk
161
+ // reporting only its own existence is a desk nobody reads by week three.
162
+ const summary = run.summary?.trim() || null;
163
+ const links = run.status === "awaiting-approval" ? approvalLinks(tenant, workspace, run.id) : null;
164
+ // At a gate the reader is being asked something; the question goes in the
165
+ // mail, and so does what the run has concluded so far — the last finished
166
+ // step's first line, which is the proposal's own summary ("… ·
167
+ // TARGETS-PROPOSAL: 1 change"). Without these a gate email said only
168
+ // that a step was waiting, and the decision meant opening the dashboard.
169
+ const asks = waitingSteps.map((s) => s.ask?.trim()).filter((a) => Boolean(a));
170
+ const soFar = run.status === "awaiting-approval" && !summary
171
+ ? [...run.steps].reverse().find((s) => s.status === "completed" && s.result?.trim())?.result?.trim().split("\n")[0] ?? null
172
+ : null;
173
+ const body = {
174
+ // `text` is what Slack-shaped receivers render; the rest is for anything
175
+ // that wants the data instead of the sentence.
176
+ text: summary
177
+ ? `${headline} — ${summary} · ${workspace}/${run.id} · $${runCost(run).toFixed(4)}`
178
+ : `${headline} — ${workspace}/${run.id} · $${runCost(run).toFixed(4)}`,
179
+ workspace,
180
+ runId: run.id,
181
+ flow: run.flow,
182
+ status: run.status,
183
+ summary,
184
+ costUsd: runCost(run),
185
+ startedAt: run.startedAt,
186
+ finishedAt: run.finishedAt,
187
+ ...(links ?? {}),
188
+ };
189
+ try {
190
+ if (config.email) {
191
+ const mail = notifyMail(tenant);
192
+ if (!mail) {
193
+ console.error(`[foldrun] notify: email configured but no mail credential — set RESEND_API_KEY (and EMAIL_FROM) on the account ${tenant}, or FOLDRUN_RESEND_API_KEY on the platform`);
194
+ return false;
195
+ }
196
+ const res = await fetch("https://api.resend.com/emails", {
197
+ method: "POST",
198
+ headers: { authorization: `Bearer ${mail.key}`, "content-type": "application/json" },
199
+ body: JSON.stringify({
200
+ from: mail.from,
201
+ to: config.email,
202
+ // The subject is the headline and what the run concluded, and
203
+ // nothing else. Ids and costs belong in the body: a subject line is
204
+ // read in a list, where the only useful question it can answer is
205
+ // whether this one needs opening.
206
+ subject: summary ? `${headline} — ${summary}`.slice(0, 160) : headline,
207
+ text: `${headline}\n` +
208
+ (summary ? `\n${summary}\n` : soFar ? `\n${soFar}\n` : "") +
209
+ (asks.length ? `\n${asks.map((a) => `Question: ${a}`).join("\n")}\n` : "") +
210
+ `\nworkspace: ${workspace}\nrun: ${run.id}\nflow: ${run.flow}\n` +
211
+ `cost: $${runCost(run).toFixed(4)}\nstarted: ${run.startedAt}\nfinished: ${run.finishedAt ?? "-"}\n` +
212
+ (links ? `\nApprove: ${links.approveUrl}\nReject: ${links.rejectUrl}\n` : ""),
213
+ }),
214
+ signal: AbortSignal.timeout(8000),
215
+ });
216
+ if (!res.ok) {
217
+ // Resend's body says WHY — "domain not verified", "can only send to
218
+ // your own address" — and a status alone sent someone to the wrong
219
+ // dashboard for twenty minutes on 2026-09-06.
220
+ const why = (await res.text().catch(() => "")).replace(/\s+/g, " ").slice(0, 300);
221
+ console.error(`[foldrun] notify email: ${tenant}/${workspace} → HTTP ${res.status} ${why}`);
222
+ return false;
223
+ }
224
+ return true;
225
+ }
226
+ // ${SECRET} so the Slack URL — itself a credential — can live in the
227
+ // vault instead of in a file that gets committed.
228
+ const url = (config.url ?? "").replace(/\$\{([A-Z][A-Z0-9_]*)\}/g, (whole, name) => {
229
+ const hit = getSecret(tenant, name, workspace);
230
+ return hit ? hit.value : whole;
231
+ });
232
+ if (!url || url.includes("${")) {
233
+ console.error(`[foldrun] notify: secret in URL not set for ${tenant}/${workspace}`);
234
+ return false;
235
+ }
236
+ const res = await fetch(url, {
237
+ method: "POST",
238
+ headers: { "content-type": "application/json" },
239
+ body: JSON.stringify(body),
240
+ signal: AbortSignal.timeout(5000),
241
+ });
242
+ if (!res.ok) {
243
+ console.error(`[foldrun] notify: ${tenant}/${workspace} → HTTP ${res.status}`);
244
+ return false;
245
+ }
246
+ return true;
247
+ }
248
+ catch (err) {
249
+ console.error(`[foldrun] notify: ${tenant}/${workspace} →`, err instanceof Error ? err.message : err);
250
+ return false;
251
+ }
252
+ }
@@ -0,0 +1,13 @@
1
+ export interface OAuthPreset {
2
+ authorize_url: string;
3
+ token_url: string;
4
+ /** Extra authorize-URL params (Google: access_type=offline&prompt=consent). */
5
+ authorize_extra?: Record<string, string>;
6
+ /** Scopes to start from — every provider names them differently, and a
7
+ * form with the right shape in it is worth more than a blank field. */
8
+ scopes_example?: string;
9
+ /** The one thing about this provider that costs an afternoon to learn. */
10
+ hint: string;
11
+ }
12
+ export declare const OAUTH_PRESETS: Record<string, OAuthPreset>;
13
+ export declare const OAUTH_PRESET_NAMES: string[];
@@ -0,0 +1,39 @@
1
+ // Prefills for the common OAuth providers: URLs and quirks, never
2
+ // credentials. One list, read by the CLI (`foldrun connect`), the dashboard's
3
+ // Connections form and the platform's consent callback, so a provider's
4
+ // refresh-token quirk is written down once.
5
+ //
6
+ // What stays with the account is the OAuth *client* (id + secret): a consent
7
+ // screen belongs to a registered app, so the developer brings those two
8
+ // values from the provider's console and everything else is here.
9
+ export const OAUTH_PRESETS = {
10
+ google: {
11
+ authorize_url: "https://accounts.google.com/o/oauth2/v2/auth",
12
+ token_url: "https://oauth2.googleapis.com/token",
13
+ authorize_extra: { access_type: "offline", prompt: "consent" },
14
+ scopes_example: "https://www.googleapis.com/auth/adwords",
15
+ hint: "Scopes are full URLs. Offline access is requested for you, so a refresh token comes back.",
16
+ },
17
+ github: {
18
+ authorize_url: "https://github.com/login/oauth/authorize",
19
+ token_url: "https://github.com/login/oauth/access_token",
20
+ scopes_example: "repo read:org",
21
+ hint: "GitHub issues long-lived tokens and no refresh token — stored as a static secret.",
22
+ },
23
+ microsoft: {
24
+ authorize_url: "https://login.microsoftonline.com/common/oauth2/v2.0/authorize",
25
+ token_url: "https://login.microsoftonline.com/common/oauth2/v2.0/token",
26
+ scopes_example: "offline_access https://graph.microsoft.com/.default",
27
+ hint: "Include offline_access in the scopes or no refresh token is issued.",
28
+ },
29
+ linkedin: {
30
+ authorize_url: "https://www.linkedin.com/oauth/v2/authorization",
31
+ token_url: "https://www.linkedin.com/oauth/v2/accessToken",
32
+ scopes_example: "openid profile r_organization_admin r_organization_social w_organization_social",
33
+ hint: "Posting as a company page needs w_organization_social and a page admin signing in; r_organization_admin lets the token list the pages it may post to. " +
34
+ "Access tokens last 60 days; a refresh token is issued only to apps LinkedIn has enabled for it, " +
35
+ "otherwise the token is stored as is and you connect again when it expires. " +
36
+ "The redirect URL must match what the app registers exactly, and only localhost may be plain http.",
37
+ },
38
+ };
39
+ export const OAUTH_PRESET_NAMES = Object.keys(OAUTH_PRESETS);
@@ -0,0 +1,220 @@
1
+ export declare const OKF_VERSION = "0.2";
2
+ /** This producer's actor string, in the spec's `<producer>/<version>` form. */
3
+ export declare const PRODUCER = "foldrun/0.1.0";
4
+ /**
5
+ * Our stand-in when a v0.1 document carries only `timestamp`. Not an actor —
6
+ * it says we don't know who, which is different from knowing it was a machine.
7
+ */
8
+ export declare const UNKNOWN_ACTOR = "unknown (v0.1 timestamp)";
9
+ /**
10
+ * §7 defines three actor forms, and only one of them is a person:
11
+ *
12
+ * human:<id> a person → human
13
+ * <producer>/<version> an agent or tool → machine
14
+ * process:<id> an automated job → machine
15
+ *
16
+ * So the test is for `human:`, not for any machine form. Testing the other way
17
+ * round meant matching a literal "producer/" prefix — but the producer's *name*
18
+ * is the first segment, so the spec's own `reference_agent/gemini-2.5-pro` did
19
+ * not match, `process:` did not match, and neither did this platform's own
20
+ * PRODUCER. The mark existed and fired for nothing real.
21
+ */
22
+ export declare const isHumanActor: (actor: string) => boolean;
23
+ export type TrustTier = "unverified" | "machine-confirmed" | "human-reviewed";
24
+ export type OkfStatus = "draft" | "stable" | "deprecated";
25
+ export interface OkfSource {
26
+ resource: string;
27
+ id?: string;
28
+ title?: string;
29
+ author?: string;
30
+ usageCount?: number;
31
+ lastModified?: string;
32
+ /** Per-entry override of the shared window, per §sources. */
33
+ usageWindow?: {
34
+ from?: string;
35
+ to?: string;
36
+ };
37
+ }
38
+ /**
39
+ * An `Attested Computation` contract. The spec binds a named computation to
40
+ * typed parameters and names an attester that mechanically checks the run
41
+ * matched the declaration — no signatures, just deterministic comparison.
42
+ *
43
+ * The runtime half (receipt and verdict wire formats, the lifecycle around a
44
+ * run) is explicitly deferred to a future spec revision, so no consumer can
45
+ * evaluate a verdict yet. What a consumer CAN do today is what the spec
46
+ * already requires: surface an unattested computation rather than presenting
47
+ * its output as though it had been checked.
48
+ */
49
+ export interface OkfComputation {
50
+ runtime: string;
51
+ parameters: {
52
+ name: string;
53
+ type: string;
54
+ required: boolean;
55
+ }[];
56
+ computation: string | null;
57
+ executorResource: string | null;
58
+ receiptFields: string[];
59
+ attesterResource: string | null;
60
+ /** Why this contract can't be attested right now, if it can't. */
61
+ attestationIssues: string[];
62
+ }
63
+ export interface OkfDoc {
64
+ /** Path within the bundle — the spec makes the path the identity, so a
65
+ * nested concept is `tables/orders.md`, not `orders.md`. */
66
+ file: string;
67
+ type: string;
68
+ title: string;
69
+ description: string;
70
+ tags: string[];
71
+ status: OkfStatus;
72
+ staleAfter: string | null;
73
+ stale: boolean;
74
+ generatedBy: string | null;
75
+ /** When it was produced, from `generated.at`. */
76
+ generatedAt: string | null;
77
+ /** Every `verified` entry, with its timestamp where one was given. */
78
+ verified: OkfActor[];
79
+ /** Just the actors — what the trust tier is computed from. */
80
+ verifiedBy: string[];
81
+ /** The most recent verification, or null if nobody dated theirs. */
82
+ verifiedAt: string | null;
83
+ trust: TrustTier;
84
+ /** Recommended v0.1 fields. */
85
+ resource: string | null;
86
+ timestamp: string | null;
87
+ sources: OkfSource[];
88
+ /** Present only on `type: Attested Computation` concepts. */
89
+ computation: OkfComputation | null;
90
+ }
91
+ /**
92
+ * The signals that decide whether a concept is worth opening: who produced it,
93
+ * and whether anyone has confirmed it.
94
+ *
95
+ * v0.2's argument for putting these in frontmatter is that most interactions
96
+ * with a concept never reach its body — a consumer first has to judge relevance
97
+ * and trustworthiness, and should be able to do that without paying for prose.
98
+ * That only works if the signals actually reach the index. `generated` was
99
+ * parsed and stored here and then surfaced nowhere, so on disk an agent's
100
+ * invention and a person's verified note were distinguishable and at the moment
101
+ * of choosing between them they were not — which is the problem the field was
102
+ * added to solve.
103
+ *
104
+ * Only the machine case is named. A person writing is the default assumption,
105
+ * and a mark on every line costs tokens on the one thing whose whole job is to
106
+ * be cheap to scan.
107
+ *
108
+ * One definition, used by both index builders. They render different prose
109
+ * around it on purpose — the memory index speaks to a model mid-run — but they
110
+ * must not disagree about what the signals *are*.
111
+ */
112
+ export declare function provenanceMarks(doc: Pick<OkfDoc, "generatedBy" | "trust"> & {
113
+ verifiedAt?: string | null;
114
+ }): string[];
115
+ /** Derived, never stored — the spec is explicit that consumers compute this. */
116
+ export declare function trustTier(verifiedBy: string[]): TrustTier;
117
+ /**
118
+ * An actor and when it acted — `{ by: human:kliu@acme, at: 2026-07-01T16:00Z }`.
119
+ *
120
+ * `at` is the difference between a fact somebody checked last week and one
121
+ * somebody checked in 2019. Both used to read as "human-reviewed", which makes
122
+ * the tier answer a weaker question than it appears to: not "can I rely on
123
+ * this" but "did anyone ever look".
124
+ */
125
+ export interface OkfActor {
126
+ by: string;
127
+ at: string | null;
128
+ }
129
+ /** The most recent `at` among these actors, or null if none carried one. */
130
+ export declare function latestAt(list: OkfActor[]): string | null;
131
+ export declare function readDoc(dir: string, file: string, today?: Date): OkfDoc | null;
132
+ /**
133
+ * Every concept in a bundle, including nested directories — Google's own
134
+ * example is a tree (`sales/tables/orders.md`), and the path is the identity.
135
+ * @param depth guards against a pathological tree; bundles are documentation,
136
+ * not filesystems.
137
+ */
138
+ export declare function readBundle(dir: string, today?: Date, prefix?: string, depth?: number): OkfDoc[];
139
+ /**
140
+ * Files that would fail somebody else's conformance check, with the reason.
141
+ *
142
+ * Deliberately walks the directory rather than readBundle(): readBundle answers
143
+ * "what should this platform show", and the two questions differ by exactly the
144
+ * files we chose not to display. A validator that isn't ours applies the spec's
145
+ * reserved set, so this one has to as well — otherwise the check agrees with us
146
+ * about a bundle a consumer will reject.
147
+ */
148
+ export declare function conformanceIssues(dir: string, prefix?: string, depth?: number): {
149
+ file: string;
150
+ issue: string;
151
+ }[];
152
+ /**
153
+ * The bundle's index.md. Progressive disclosure, per the spec: headings and
154
+ * bulleted links with descriptions, and no frontmatter — except `okf_version`
155
+ * at a bundle root, which is the only place the spec permits it.
156
+ */
157
+ export declare function buildIndex(docs: OkfDoc[], title: string, isRoot: boolean, children?: string[]): string;
158
+ /**
159
+ * Write index.md for a bundle and for every directory inside it. The spec puts
160
+ * an index at each level, so a nested bundle is navigable without a full walk.
161
+ */
162
+ export declare function syncIndex(dir: string, title: string, isRoot?: boolean, depth?: number): void;
163
+ /**
164
+ * Dates the platform had to drop, with where they were.
165
+ *
166
+ * Deliberately not part of conformanceIssues. That function answers one
167
+ * question — would an outside validator accept this bundle — and the spec's
168
+ * three rules say nothing about the *shape* of a date, so a bad one is
169
+ * conformant and still unusable here. Two questions, two functions, and the
170
+ * conformance answer stays exactly the spec's.
171
+ */
172
+ export declare function dateIssues(dir: string, prefix?: string, depth?: number): {
173
+ file: string;
174
+ field: string;
175
+ value: string;
176
+ }[];
177
+ /**
178
+ * Stamp provenance on every concept in a bundle an agent has just written to.
179
+ *
180
+ * Recursive, and it skips the spec's reserved names. Doing this by hand with a
181
+ * flat readdir and an `!== "index.md"` filter missed every concept in a nested
182
+ * section, and — worse — stamped `log.md`, writing a concept's frontmatter onto
183
+ * a reserved file that §9 gives its own structure. Nothing reported that,
184
+ * because a conformance check skips reserved names by definition: the file
185
+ * ends up malformed in a way only its readers notice.
186
+ *
187
+ * Returns the bundle-relative paths it stamped, so the caller can record each
188
+ * one in the log. Returning a bare boolean meant the caller had nothing to
189
+ * name, so it re-synced the index and wrote no log entry at all — and a run
190
+ * that taught the agent something left no trace in the one file whose job is
191
+ * to say what changed and when.
192
+ */
193
+ export declare function stampBundle(dir: string, agent: string | null, at?: Date, prefix?: string, depth?: number): string[];
194
+ /**
195
+ * Make every OKF bundle in a workspace a bundle.
196
+ *
197
+ * A concept file with a `type` is conformant on its own, but a *bundle* is
198
+ * only self-describing once its root index.md exists to carry `okf_version`.
199
+ * Both scaffold paths wrote their starter files straight to disk, so a new
200
+ * workspace's knowledge/ was a directory of valid concepts and nothing that
201
+ * said which version of the format they were written against — the one thing
202
+ * a consumer reads first.
203
+ *
204
+ * An agent's own pair is nested rather than a root, so it gets an index
205
+ * without a version, which is what the spec permits.
206
+ */
207
+ export declare function syncWorkspaceBundles(root: string): void;
208
+ /**
209
+ * log.md — the bundle's change history. The spec asks for date-grouped
210
+ * entries, newest first, ISO 8601 headings, with the **Creation** / **Update**
211
+ * convention. Appending here means the history is a real artifact rather than
212
+ * something you reconstruct from git.
213
+ */
214
+ export declare function appendLog(dir: string, file: string, kind: "Creation" | "Update", at?: Date): void;
215
+ /**
216
+ * Stamp provenance on a file that doesn't declare it. Used after a run so a
217
+ * memory an agent wrote is attributable — without this, a fact the model
218
+ * invented and a fact a person verified are indistinguishable on disk.
219
+ */
220
+ export declare function stampGenerated(file: string, agent: string | null, at?: Date): boolean;