@foldrun/core 0.4.0 → 0.6.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 (170) hide show
  1. package/README.md +7 -4
  2. package/dist/index.d.ts +4 -0
  3. package/dist/index.js +4 -0
  4. package/dist/src/account.d.ts +45 -0
  5. package/dist/src/account.js +130 -0
  6. package/dist/src/api-tools.d.ts +8 -0
  7. package/dist/src/api-tools.js +22 -4
  8. package/dist/src/browser-pod.d.ts +148 -0
  9. package/dist/src/browser-pod.js +333 -0
  10. package/dist/src/catalog.js +2 -1
  11. package/dist/src/clock.d.ts +49 -0
  12. package/dist/src/clock.js +121 -0
  13. package/dist/src/completions.js +17 -8
  14. package/dist/src/confine.d.ts +22 -0
  15. package/dist/src/confine.js +218 -3
  16. package/dist/src/container.d.ts +9 -0
  17. package/dist/src/container.js +115 -5
  18. package/dist/src/context-tools.d.ts +1 -1
  19. package/dist/src/deploy.d.ts +33 -0
  20. package/dist/src/deploy.js +145 -6
  21. package/dist/src/egress.d.ts +7 -0
  22. package/dist/src/egress.js +6 -0
  23. package/dist/src/eol.d.ts +2 -0
  24. package/dist/src/eol.js +7 -0
  25. package/dist/src/evals.d.ts +5 -0
  26. package/dist/src/evals.js +9 -2
  27. package/dist/src/flow-lint.d.ts +8 -0
  28. package/dist/src/flow-lint.js +223 -1
  29. package/dist/src/flow-patterns.d.ts +141 -0
  30. package/dist/src/flow-patterns.js +568 -0
  31. package/dist/src/gitrepo.d.ts +18 -0
  32. package/dist/src/gitrepo.js +49 -0
  33. package/dist/src/host-env.js +5 -0
  34. package/dist/src/kinds.js +1 -1
  35. package/dist/src/language.d.ts +17 -0
  36. package/dist/src/language.js +55 -0
  37. package/dist/src/layout.d.ts +69 -0
  38. package/dist/src/layout.js +200 -0
  39. package/dist/src/library.d.ts +9 -0
  40. package/dist/src/library.js +30 -3
  41. package/dist/src/limits.d.ts +84 -0
  42. package/dist/src/limits.js +287 -0
  43. package/dist/src/locale.d.ts +47 -0
  44. package/dist/src/locale.js +177 -0
  45. package/dist/src/mail.d.ts +88 -0
  46. package/dist/src/mail.js +247 -0
  47. package/dist/src/model-credential.d.ts +14 -0
  48. package/dist/src/model-credential.js +58 -0
  49. package/dist/src/model-probe.d.ts +3 -0
  50. package/dist/src/model-probe.js +1 -0
  51. package/dist/src/notify.d.ts +53 -7
  52. package/dist/src/notify.js +194 -126
  53. package/dist/src/openapi.js +5 -3
  54. package/dist/src/operator.d.ts +201 -0
  55. package/dist/src/operator.js +251 -0
  56. package/dist/src/paths.d.ts +28 -0
  57. package/dist/src/paths.js +55 -0
  58. package/dist/src/platform.d.ts +43 -3
  59. package/dist/src/platform.js +9 -1
  60. package/dist/src/provider-health.js +3 -2
  61. package/dist/src/providers.d.ts +481 -0
  62. package/dist/src/providers.js +1105 -6
  63. package/dist/src/refusal.d.ts +83 -0
  64. package/dist/src/refusal.js +281 -0
  65. package/dist/src/run-container.d.ts +149 -13
  66. package/dist/src/run-container.js +424 -43
  67. package/dist/src/runner.d.ts +177 -9
  68. package/dist/src/runner.js +1146 -154
  69. package/dist/src/runtime.d.ts +52 -0
  70. package/dist/src/runtime.js +493 -58
  71. package/dist/src/script-tools.d.ts +17 -2
  72. package/dist/src/script-tools.js +82 -10
  73. package/dist/src/secret-files.d.ts +6 -5
  74. package/dist/src/secret-files.js +24 -11
  75. package/dist/src/secret-health.d.ts +30 -6
  76. package/dist/src/secret-health.js +42 -20
  77. package/dist/src/secrets.d.ts +19 -0
  78. package/dist/src/secrets.js +155 -86
  79. package/dist/src/starter.d.ts +9 -0
  80. package/dist/src/starter.js +92 -11
  81. package/dist/src/step-exec.d.ts +72 -2
  82. package/dist/src/step-exec.js +431 -37
  83. package/dist/src/step-removal.d.ts +15 -0
  84. package/dist/src/step-removal.js +49 -0
  85. package/dist/src/storage.d.ts +23 -4
  86. package/dist/src/storage.js +125 -23
  87. package/dist/src/store.d.ts +222 -37
  88. package/dist/src/store.js +349 -62
  89. package/dist/src/subagents.d.ts +75 -0
  90. package/dist/src/subagents.js +149 -0
  91. package/dist/src/test-mode.d.ts +17 -2
  92. package/dist/src/test-mode.js +53 -13
  93. package/dist/src/tool-names.d.ts +13 -1
  94. package/dist/src/tool-names.js +38 -11
  95. package/dist/src/tool-programs.d.ts +26 -0
  96. package/dist/src/tool-programs.js +172 -0
  97. package/dist/src/tool-test.js +104 -12
  98. package/dist/src/translator.d.ts +8 -2
  99. package/dist/src/translator.js +69 -23
  100. package/dist/src/trigger-log.d.ts +1 -1
  101. package/dist/src/triggers.d.ts +3 -0
  102. package/dist/src/triggers.js +10 -2
  103. package/dist/src/untrusted-fetch.d.ts +11 -0
  104. package/dist/src/untrusted-fetch.js +77 -0
  105. package/dist/src/web-actions.d.ts +91 -0
  106. package/dist/src/web-actions.js +199 -0
  107. package/dist/src/webhook.d.ts +4 -0
  108. package/dist/src/webhook.js +39 -0
  109. package/index.ts +4 -0
  110. package/package.json +3 -3
  111. package/src/account.ts +149 -0
  112. package/src/api-tools.ts +32 -4
  113. package/src/browser-pod.ts +350 -0
  114. package/src/catalog.ts +2 -1
  115. package/src/clock.ts +149 -0
  116. package/src/completions.ts +17 -8
  117. package/src/confine.ts +218 -3
  118. package/src/container.ts +112 -5
  119. package/src/context-tools.ts +1 -1
  120. package/src/deploy.ts +137 -6
  121. package/src/egress.ts +13 -1
  122. package/src/eol.ts +7 -0
  123. package/src/evals.ts +14 -2
  124. package/src/flow-lint.ts +253 -1
  125. package/src/flow-patterns.ts +597 -0
  126. package/src/gitrepo.ts +48 -0
  127. package/src/host-env.ts +5 -0
  128. package/src/kinds.ts +1 -1
  129. package/src/language.ts +62 -0
  130. package/src/layout.ts +228 -0
  131. package/src/library.ts +37 -3
  132. package/src/limits.ts +290 -0
  133. package/src/locale.ts +217 -0
  134. package/src/mail.ts +318 -0
  135. package/src/model-credential.ts +71 -0
  136. package/src/model-probe.ts +4 -0
  137. package/src/notify.ts +189 -103
  138. package/src/openapi.ts +5 -3
  139. package/src/operator.ts +300 -0
  140. package/src/paths.ts +54 -0
  141. package/src/platform.ts +52 -3
  142. package/src/provider-health.ts +3 -2
  143. package/src/providers.ts +1368 -6
  144. package/src/refusal.ts +320 -0
  145. package/src/run-container.ts +460 -44
  146. package/src/runner.ts +1168 -155
  147. package/src/runtime.ts +487 -55
  148. package/src/script-tools.ts +81 -10
  149. package/src/secret-files.ts +22 -10
  150. package/src/secret-health.ts +64 -19
  151. package/src/secrets.ts +149 -87
  152. package/src/starter.ts +93 -11
  153. package/src/step-exec.ts +447 -38
  154. package/src/step-removal.ts +59 -0
  155. package/src/storage.ts +141 -22
  156. package/src/store.ts +506 -70
  157. package/src/subagents.ts +189 -0
  158. package/src/test-mode.ts +51 -11
  159. package/src/tool-names.ts +40 -11
  160. package/src/tool-programs.ts +179 -0
  161. package/src/tool-test.ts +110 -12
  162. package/src/translator.ts +72 -22
  163. package/src/trigger-log.ts +1 -0
  164. package/src/triggers.ts +10 -2
  165. package/src/untrusted-fetch.ts +88 -0
  166. package/src/web-actions.ts +237 -0
  167. package/src/webhook.ts +41 -0
  168. package/templates/blog-desk/agents/writer/agent.md +1 -1
  169. package/templates/hello/agents/curator/agent.md +4 -4
  170. package/templates/hello/agents/notetaker/agent.md +3 -3
@@ -0,0 +1,17 @@
1
+ export interface LanguageChoice {
2
+ /** BCP-47, normalised: `en`, `en-AU`, `fa`, `pt-BR`. */
3
+ language: string;
4
+ from: "agent" | "flow" | "workspace" | "account" | "env" | "default";
5
+ /** A level that wrote something unreadable, said once and skipped. */
6
+ lines: string[];
7
+ }
8
+ /** `en`, `en-AU`, `en_au`, ` FA-ir ` → `en`, `en-AU`, `en-AU`, `fa-IR`; anything else null. */
9
+ export declare function normalizeLanguage(raw: unknown): string | null;
10
+ /** Null when unset (inherit) or readable; else the sentence check prints. */
11
+ export declare function languageProblem(raw: unknown): string | null;
12
+ export declare function resolveLanguage(levels: {
13
+ level: LanguageChoice["from"];
14
+ value: unknown;
15
+ }[], env?: Record<string, string | undefined>): LanguageChoice;
16
+ /** How a person would name it in a prompt: `fa-IR` → "Persian (Iran)". */
17
+ export declare function languageName(tag: string): string;
@@ -0,0 +1,55 @@
1
+ // The language an agent works in — the same cascade as its clock, for the
2
+ // same reason: a value written once at the level it belongs to, inherited
3
+ // below it, and never a hardcoded default inside a tool.
4
+ //
5
+ // Prose in AGENTS.md reaches the model and nothing else. A search engine
6
+ // asked in English answers in English whatever the agent was told, a
7
+ // browser reports en-US to every page, a fetch sends no Accept-Language.
8
+ // `language:` is what those read; the prompt sentence it adds is the small
9
+ // part. English is the default, so nothing changes for an agent that never
10
+ // says.
11
+ /** `en`, `en-AU`, `en_au`, ` FA-ir ` → `en`, `en-AU`, `en-AU`, `fa-IR`; anything else null. */
12
+ export function normalizeLanguage(raw) {
13
+ if (typeof raw !== "string")
14
+ return null;
15
+ const m = /^([a-z]{2,3})(?:[-_]([a-z]{2}))?$/i.exec(raw.trim());
16
+ if (!m)
17
+ return null;
18
+ return m[2] ? `${m[1].toLowerCase()}-${m[2].toUpperCase()}` : m[1].toLowerCase();
19
+ }
20
+ /** Null when unset (inherit) or readable; else the sentence check prints. */
21
+ export function languageProblem(raw) {
22
+ if (raw === undefined || raw === null || raw === "")
23
+ return null;
24
+ if (normalizeLanguage(raw))
25
+ return null;
26
+ return `language: ${JSON.stringify(raw)} is not a language tag — write it like \`en\`, \`en-AU\` or \`fa\` (a two-letter language, and optionally a two-letter region).`;
27
+ }
28
+ export function resolveLanguage(levels, env = process.env) {
29
+ const lines = [];
30
+ for (const { level, value } of levels) {
31
+ if (value === undefined || value === null || value === "")
32
+ continue;
33
+ const tag = normalizeLanguage(value);
34
+ if (tag)
35
+ return { language: tag, from: level, lines };
36
+ lines.push(`language: ${level} says ${JSON.stringify(value)}, which is not a language tag — using the next level`);
37
+ }
38
+ const fromEnv = normalizeLanguage(env.FOLDRUN_LANGUAGE);
39
+ if (fromEnv)
40
+ return { language: fromEnv, from: "env", lines };
41
+ return { language: "en", from: "default", lines };
42
+ }
43
+ /** How a person would name it in a prompt: `fa-IR` → "Persian (Iran)". */
44
+ export function languageName(tag) {
45
+ try {
46
+ const [lang, region] = tag.split("-");
47
+ const names = new Intl.DisplayNames(["en"], { type: "language" });
48
+ const regions = new Intl.DisplayNames(["en"], { type: "region" });
49
+ const base = names.of(lang) ?? lang;
50
+ return region ? `${base} (${regions.of(region) ?? region})` : base;
51
+ }
52
+ catch {
53
+ return tag;
54
+ }
55
+ }
@@ -0,0 +1,69 @@
1
+ /** Where an account keeps its workspaces. `projects/` is the pre-rename name,
2
+ * still read so an existing tree keeps working. */
3
+ export declare const WORKSPACES_DIRS: readonly ["workspaces", "projects"];
4
+ /**
5
+ * A workspace is a directory holding `agents/` or `flows/`.
6
+ *
7
+ * Not AGENTS.md: an account root has one of those too, and a test that cannot
8
+ * tell the two apart is the bug this module exists to prevent.
9
+ */
10
+ export declare function isWorkspaceDir(dir: string): boolean;
11
+ /** `<dir>/workspaces` when `dir` is an account root, else null. */
12
+ export declare function accountWorkspacesDir(dir: string): string | null;
13
+ /** The workspaces under an account root, by name, sorted. */
14
+ export declare function listWorkspaceDirs(accountRoot: string): string[];
15
+ export type LayoutKind = "account" | "workspace-in-account" | "flat" | "empty";
16
+ export interface Layout {
17
+ /**
18
+ * `account` — an account folder, pointed at its root.
19
+ * `workspace-in-account` — inside one of its workspaces.
20
+ * `flat` — a lone workspace, the shape `foldrun init` made until today.
21
+ * `empty` — neither: a bare or brand-new directory.
22
+ */
23
+ kind: LayoutKind;
24
+ /** Where AGENTS.md and `library/` live — the account scope. */
25
+ accountRoot: string;
26
+ /** `<accountRoot>/workspaces`, or null when there is no account layout. */
27
+ workspacesDir: string | null;
28
+ /** The workspace we are in, absolute. Null at an account root. */
29
+ workspaceDir: string | null;
30
+ /** Its name. Null at an account root. */
31
+ workspace: string | null;
32
+ /** Every workspace this account holds, by name. A flat folder holds one. */
33
+ workspaces: string[];
34
+ }
35
+ /**
36
+ * Which of the two shapes `from` is in, and the directories that follow from
37
+ * the answer.
38
+ *
39
+ * Walks up, remembering the first workspace directory it passes and stopping
40
+ * at the first account root. A path anywhere inside a workspace resolves to
41
+ * that workspace, so `foldrun status` works from `agents/writer/` the way git
42
+ * works from anywhere in a checkout.
43
+ */
44
+ export declare function detectLayout(from: string): Layout;
45
+ /**
46
+ * The account scope for a workspace directory.
47
+ *
48
+ * For a flat workspace that is simply its parent — `my-desk/` sits beside the
49
+ * `AGENTS.md` and `library/` that cover it, which is what `libraryDir` has
50
+ * always assumed. For a workspace inside an account it is two levels up, NOT
51
+ * one: one level up is `workspaces/`, which holds no library and no config,
52
+ * and pointing the account scope there is how a local account folder used to
53
+ * lose its shared library the moment the runtime asked for it.
54
+ */
55
+ export declare function accountRootFor(workspaceDir: string): string;
56
+ /**
57
+ * The data root of the installation this account belongs to, or null when the
58
+ * account is a folder someone made on their laptop.
59
+ *
60
+ * `<data>/<tenant>/workspaces/<name>` and `<my-account>/workspaces/<name>` are
61
+ * the same shape, so the shape cannot tell them apart. What can is the
62
+ * installation's own marker, which sits at the data root and nowhere else:
63
+ * `.foldrun-install`, written by the platform at boot. `.secret-key` and
64
+ * `keys.json` are the older markers — kept for installs from before it, and
65
+ * no longer enough alone, because keys move into the database and a key held
66
+ * in FOLDRUN_SECRET_KEY never had a file.
67
+ */
68
+ export declare const INSTALL_MARKER = ".foldrun-install";
69
+ export declare function installationDataRoot(accountRoot: string): string | null;
@@ -0,0 +1,200 @@
1
+ // What shape is this folder?
2
+ //
3
+ // There are two shapes a foldrun tree on a laptop can have, and every command
4
+ // has to answer the same question about the one it was pointed at.
5
+ //
6
+ // flat account
7
+ // my-desk/ my-account/
8
+ // ├── AGENTS.md ├── AGENTS.md ← account scope
9
+ // ├── agents/ ├── library/ ← shared by every workspace
10
+ // └── flows/ └── workspaces/
11
+ // └── my-desk/
12
+ // ├── AGENTS.md
13
+ // ├── agents/
14
+ // └── flows/
15
+ //
16
+ // The second is the platform's own layout: `<data>/<account>/library` and
17
+ // `<data>/<account>/workspaces/<name>` are exactly these directories with a
18
+ // tenant directory around them. The first is what `foldrun init` made until
19
+ // today, so it is what every existing user has, and it must keep working
20
+ // forever — a folder shape is a file format.
21
+ //
22
+ // This module is the ONE place that decides. Everything else — the CLI's
23
+ // commands, `accountDir`, `libraryDir` — asks here rather than testing for a
24
+ // `workspaces` directory itself, because the last time that test was written
25
+ // inline it was written as "is my parent called workspaces", which is true of
26
+ // a real installation and of this layout and means something different in
27
+ // each.
28
+ //
29
+ // Deliberately free of every other import: `paths.ts` needs it, and `paths.ts`
30
+ // is what the rest of the codebase imports first.
31
+ import fs from "node:fs";
32
+ import path from "node:path";
33
+ /** Where an account keeps its workspaces. `projects/` is the pre-rename name,
34
+ * still read so an existing tree keeps working. */
35
+ export const WORKSPACES_DIRS = ["workspaces", "projects"];
36
+ function isDir(p) {
37
+ try {
38
+ return fs.statSync(p).isDirectory();
39
+ }
40
+ catch {
41
+ return false;
42
+ }
43
+ }
44
+ /**
45
+ * A workspace is a directory holding `agents/` or `flows/`.
46
+ *
47
+ * Not AGENTS.md: an account root has one of those too, and a test that cannot
48
+ * tell the two apart is the bug this module exists to prevent.
49
+ */
50
+ export function isWorkspaceDir(dir) {
51
+ return isDir(path.join(dir, "agents")) || isDir(path.join(dir, "flows"));
52
+ }
53
+ /** `<dir>/workspaces` when `dir` is an account root, else null. */
54
+ export function accountWorkspacesDir(dir) {
55
+ for (const name of WORKSPACES_DIRS) {
56
+ const full = path.join(dir, name);
57
+ if (isDir(full))
58
+ return full;
59
+ }
60
+ return null;
61
+ }
62
+ /** The workspaces under an account root, by name, sorted. */
63
+ export function listWorkspaceDirs(accountRoot) {
64
+ const dir = accountWorkspacesDir(accountRoot);
65
+ if (!dir)
66
+ return [];
67
+ try {
68
+ return fs
69
+ .readdirSync(dir)
70
+ .filter((n) => !n.startsWith("."))
71
+ .filter((n) => isDir(path.join(dir, n)))
72
+ .sort();
73
+ }
74
+ catch {
75
+ return [];
76
+ }
77
+ }
78
+ /** How far up to look before giving up: a project root (`.git`) is a boundary
79
+ * a layout does not cross, and so is the filesystem root. */
80
+ function* upward(from) {
81
+ let dir = path.resolve(from);
82
+ for (;;) {
83
+ yield dir;
84
+ if (isDir(path.join(dir, ".git")))
85
+ return;
86
+ const up = path.dirname(dir);
87
+ if (up === dir)
88
+ return;
89
+ dir = up;
90
+ }
91
+ }
92
+ /**
93
+ * Which of the two shapes `from` is in, and the directories that follow from
94
+ * the answer.
95
+ *
96
+ * Walks up, remembering the first workspace directory it passes and stopping
97
+ * at the first account root. A path anywhere inside a workspace resolves to
98
+ * that workspace, so `foldrun status` works from `agents/writer/` the way git
99
+ * works from anywhere in a checkout.
100
+ */
101
+ export function detectLayout(from) {
102
+ const start = path.resolve(from);
103
+ let workspaceDir = null;
104
+ let accountRoot = null;
105
+ for (const dir of upward(start)) {
106
+ if (!workspaceDir && isWorkspaceDir(dir))
107
+ workspaceDir = dir;
108
+ const ws = accountWorkspacesDir(dir);
109
+ // An account root holds `workspaces/`. The workspace directory we may
110
+ // have just found must be *inside* it — a workspace that happens to have
111
+ // its own `workspaces/` folder of something else is not an account.
112
+ if (ws && (!workspaceDir || workspaceDir.startsWith(ws + path.sep))) {
113
+ accountRoot = dir;
114
+ break;
115
+ }
116
+ }
117
+ if (accountRoot) {
118
+ const workspaces = listWorkspaceDirs(accountRoot);
119
+ if (workspaceDir) {
120
+ return {
121
+ kind: "workspace-in-account",
122
+ accountRoot,
123
+ workspacesDir: accountWorkspacesDir(accountRoot),
124
+ workspaceDir,
125
+ workspace: path.basename(workspaceDir),
126
+ workspaces,
127
+ };
128
+ }
129
+ return {
130
+ kind: "account",
131
+ accountRoot,
132
+ workspacesDir: accountWorkspacesDir(accountRoot),
133
+ workspaceDir: null,
134
+ workspace: null,
135
+ workspaces,
136
+ };
137
+ }
138
+ if (workspaceDir) {
139
+ return {
140
+ kind: "flat",
141
+ accountRoot: path.dirname(workspaceDir),
142
+ workspacesDir: null,
143
+ workspaceDir,
144
+ workspace: path.basename(workspaceDir),
145
+ workspaces: [path.basename(workspaceDir)],
146
+ };
147
+ }
148
+ return {
149
+ kind: "empty",
150
+ accountRoot: path.dirname(start),
151
+ workspacesDir: null,
152
+ workspaceDir: start,
153
+ workspace: path.basename(start),
154
+ workspaces: [],
155
+ };
156
+ }
157
+ /**
158
+ * The account scope for a workspace directory.
159
+ *
160
+ * For a flat workspace that is simply its parent — `my-desk/` sits beside the
161
+ * `AGENTS.md` and `library/` that cover it, which is what `libraryDir` has
162
+ * always assumed. For a workspace inside an account it is two levels up, NOT
163
+ * one: one level up is `workspaces/`, which holds no library and no config,
164
+ * and pointing the account scope there is how a local account folder used to
165
+ * lose its shared library the moment the runtime asked for it.
166
+ */
167
+ export function accountRootFor(workspaceDir) {
168
+ const dir = path.resolve(workspaceDir);
169
+ const parent = path.dirname(dir);
170
+ if (WORKSPACES_DIRS.includes(path.basename(parent))) {
171
+ return path.dirname(parent);
172
+ }
173
+ return parent;
174
+ }
175
+ /**
176
+ * The data root of the installation this account belongs to, or null when the
177
+ * account is a folder someone made on their laptop.
178
+ *
179
+ * `<data>/<tenant>/workspaces/<name>` and `<my-account>/workspaces/<name>` are
180
+ * the same shape, so the shape cannot tell them apart. What can is the
181
+ * installation's own marker, which sits at the data root and nowhere else:
182
+ * `.foldrun-install`, written by the platform at boot. `.secret-key` and
183
+ * `keys.json` are the older markers — kept for installs from before it, and
184
+ * no longer enough alone, because keys move into the database and a key held
185
+ * in FOLDRUN_SECRET_KEY never had a file.
186
+ */
187
+ export const INSTALL_MARKER = ".foldrun-install";
188
+ export function installationDataRoot(accountRoot) {
189
+ const above = path.dirname(path.resolve(accountRoot));
190
+ for (const marker of [INSTALL_MARKER, ".secret-key", "keys.json"]) {
191
+ try {
192
+ if (fs.statSync(path.join(above, marker)).isFile())
193
+ return above;
194
+ }
195
+ catch {
196
+ /* not this one */
197
+ }
198
+ }
199
+ return null;
200
+ }
@@ -9,6 +9,15 @@ export interface LibraryEntry {
9
9
  description: string;
10
10
  /** For skills: true when the folder bundles scripts/. */
11
11
  hasScripts?: boolean;
12
+ /**
13
+ * Every file this entry is made of, relative to the kind's directory,
14
+ * `path` included. One entry stays one tool — the shelf must not grow a
15
+ * second row called browser — but a caller copying the library needs the
16
+ * code beside the manifest, and the listing is the only place that knows
17
+ * what is in the folder. Absent for a single-file entry, where `path` is
18
+ * already the whole of it.
19
+ */
20
+ files?: string[];
12
21
  /**
13
22
  * For tools: how this one connects — http, script or mcp. Surfaced so a
14
23
  * list of tools reads as one vocabulary with three transports, rather
@@ -13,7 +13,7 @@
13
13
  import { platform } from "./platform.js";
14
14
  import fs from "node:fs";
15
15
  import path from "node:path";
16
- import { dataRoot, singleWorkspace } from "./paths.js";
16
+ import { dataRoot, singleWorkspace, singleAccountRoot, MAX_EDITABLE_FILE } from "./paths.js";
17
17
  import matter from "gray-matter";
18
18
  import { KINDS } from "./kinds.js";
19
19
  import { recordRevision, registerTreeReader } from "./history.js";
@@ -28,7 +28,9 @@ export function libraryDir(tenant, kind) {
28
28
  // library is still possible on a laptop without inventing a home directory.
29
29
  const single = singleWorkspace();
30
30
  if (single) {
31
- const base = path.join(path.resolve(single), "..", "library");
31
+ // The account scope, which is the workspace's parent only when the
32
+ // workspace is a flat one — see singleAccountRoot.
33
+ const base = path.join(singleAccountRoot(), "library");
32
34
  return kind ? path.join(base, kind) : base;
33
35
  }
34
36
  assertSafeName(tenant, "tenant");
@@ -51,6 +53,27 @@ function assertLibraryPath(kind, rel) {
51
53
  }
52
54
  return norm;
53
55
  }
56
+ /** Every file inside a folder entry, as paths relative to the kind's
57
+ * directory. Depth-first so a nested `scripts/` comes with it. */
58
+ function folderFiles(dir, prefix) {
59
+ const out = [];
60
+ const walk = (at, rel) => {
61
+ for (const e of fs.readdirSync(at, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
62
+ const here = rel ? `${rel}/${e.name}` : e.name;
63
+ if (e.isDirectory())
64
+ walk(path.join(at, e.name), here);
65
+ else
66
+ out.push(`${prefix}/${here}`);
67
+ }
68
+ };
69
+ try {
70
+ walk(dir, "");
71
+ }
72
+ catch {
73
+ return [`${prefix}/tool.md`];
74
+ }
75
+ return out;
76
+ }
54
77
  export function listLibrary(tenant, kind) {
55
78
  const dir = libraryDir(tenant, kind);
56
79
  if (!fs.existsSync(dir))
@@ -78,6 +101,7 @@ export function listLibrary(tenant, kind) {
78
101
  out.push({
79
102
  ...describe(manifest, entry.name),
80
103
  path: `${entry.name}/tool.md`,
104
+ files: folderFiles(full, entry.name),
81
105
  ...toolShape(manifest, entry.name),
82
106
  updatedAt: fs.statSync(manifest).mtime.toISOString(),
83
107
  });
@@ -104,6 +128,7 @@ export function listLibrary(tenant, kind) {
104
128
  ...describe(skillMd, entry),
105
129
  path: `${entry}/SKILL.md`,
106
130
  hasScripts: fs.existsSync(path.join(full, "scripts")),
131
+ files: folderFiles(full, entry),
107
132
  updatedAt: fs.statSync(skillMd).mtime.toISOString(),
108
133
  });
109
134
  }
@@ -167,7 +192,9 @@ export function libraryFileExists(tenant, kind, rel) {
167
192
  }
168
193
  }
169
194
  export function writeLibraryFile(tenant, kind, rel, content, meta = {}) {
170
- if (content.length > 256 * 1024)
195
+ // 512 KB: a folder tool's program is one file, and the gallery browser's is
196
+ // past 256 KB. Still a file a person edits, not a store for data.
197
+ if (content.length > MAX_EDITABLE_FILE)
171
198
  throw new Error("file too large");
172
199
  const norm = assertLibraryPath(kind, rel);
173
200
  if (norm.endsWith(".md"))
@@ -0,0 +1,84 @@
1
+ export type Limits = Record<string, number>;
2
+ /** The key that caps every call in the step. */
3
+ export declare const TOTAL_KEY = "calls";
4
+ /** One spelling for comparing: `desk-email` and `desk_email` are the same
5
+ * tool (a script tool's SDK name has its dashes made underscores). */
6
+ export declare function normKey(key: string): string;
7
+ /**
8
+ * A `limits:` block as written: the values that can be read, and a sentence
9
+ * for each that cannot. Shape only — whether a key names a tool this agent
10
+ * has is `limitKeyProblems`, which needs the grants.
11
+ */
12
+ export declare function readLimits(raw: unknown, where?: string): {
13
+ limits: Limits;
14
+ problems: string[];
15
+ };
16
+ /**
17
+ * The limits a step runs under, nearest last: account, workspace, agent,
18
+ * step. A key set nearer replaces the same key further out; the others
19
+ * stand. Unreadable values are dropped here — `check` and the deploy gate
20
+ * say so where they were written.
21
+ */
22
+ export declare function cascadeLimits(levels: unknown[]): Limits;
23
+ /** SDK tool name → the foldrun name it is granted as, for the tools whose
24
+ * SDK name does not say (an openapi tool's operations, a script whose name
25
+ * was made safe). `apiOwners` is api-tools' own record of which API each
26
+ * tool was built for — not guessed from the name, which cannot tell `crm`'s
27
+ * operation `admin_list` from `crm_admin`'s `list`. Built host-side; plain
28
+ * JSON so it crosses into a pod. */
29
+ export declare function toolOwners(apiOwners: Record<string, string>, scripts: {
30
+ name: string;
31
+ }[]): Record<string, string>;
32
+ /**
33
+ * Every key one call counts under, `calls` included. Pure: the SDK's tool
34
+ * name, its input, and the owners map.
35
+ */
36
+ export declare function limitKeysFor(toolName: string, input: Record<string, unknown> | undefined, owners?: Record<string, string>): string[];
37
+ export interface LimitRefusal {
38
+ key: string;
39
+ used: number;
40
+ limit: number;
41
+ message: string;
42
+ }
43
+ /**
44
+ * One step's counts. `take` checks every key the call counts under and
45
+ * either refuses — counting nothing — or counts it under all of them.
46
+ */
47
+ export declare class CallCounter {
48
+ private readonly limits;
49
+ private readonly used;
50
+ refused: number;
51
+ constructor(limits: Limits | undefined | null);
52
+ get active(): boolean;
53
+ take(keys: string[]): LimitRefusal | null;
54
+ /** Give back a call that was counted and then refused by a later check. */
55
+ refund(keys: string[]): void;
56
+ count(key: string): number;
57
+ /** `limits: web.search 40/40, crm 3/20` — null when no limits are set. */
58
+ summary(): string | null;
59
+ }
60
+ /**
61
+ * Keys an agent's `limits:` names that it cannot use. `known` is every tool
62
+ * name in reach (built-ins, the workspace's and library's tools, this
63
+ * agent's APIs, scripts and MCP servers); `granted` is what this agent's
64
+ * `tools:` (and `apis:`, `scripts:`, `mcpServers:`) gives it. A key that
65
+ * names nothing is an error; a real tool this agent does not hold is a
66
+ * warning — the limit is harmless, and probably a leftover.
67
+ */
68
+ export declare function limitKeyProblems(limits: Limits, ctx: {
69
+ known: Iterable<string>;
70
+ granted: Iterable<string>;
71
+ }): {
72
+ level: "error" | "warn";
73
+ message: string;
74
+ }[];
75
+ /**
76
+ * A flow step's `limits:` option, written on one line:
77
+ * `limits: {web.search: 10, calls: 50}` (the braces are optional). Read by
78
+ * hand rather than as YAML, because `web.search` is a plain key here and one
79
+ * line is all a step option has.
80
+ */
81
+ export declare function parseStepLimits(text: string): {
82
+ limits: Limits;
83
+ problems: string[];
84
+ };