@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.
- package/README.md +7 -4
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/src/account.d.ts +45 -0
- package/dist/src/account.js +130 -0
- package/dist/src/api-tools.d.ts +8 -0
- package/dist/src/api-tools.js +22 -4
- package/dist/src/browser-pod.d.ts +148 -0
- package/dist/src/browser-pod.js +333 -0
- package/dist/src/catalog.js +2 -1
- package/dist/src/clock.d.ts +49 -0
- package/dist/src/clock.js +121 -0
- package/dist/src/completions.js +17 -8
- package/dist/src/confine.d.ts +22 -0
- package/dist/src/confine.js +218 -3
- package/dist/src/container.d.ts +9 -0
- package/dist/src/container.js +115 -5
- package/dist/src/context-tools.d.ts +1 -1
- package/dist/src/deploy.d.ts +33 -0
- package/dist/src/deploy.js +145 -6
- package/dist/src/egress.d.ts +7 -0
- package/dist/src/egress.js +6 -0
- package/dist/src/eol.d.ts +2 -0
- package/dist/src/eol.js +7 -0
- package/dist/src/evals.d.ts +5 -0
- package/dist/src/evals.js +9 -2
- package/dist/src/flow-lint.d.ts +8 -0
- package/dist/src/flow-lint.js +223 -1
- package/dist/src/flow-patterns.d.ts +141 -0
- package/dist/src/flow-patterns.js +568 -0
- package/dist/src/gitrepo.d.ts +18 -0
- package/dist/src/gitrepo.js +49 -0
- package/dist/src/host-env.js +5 -0
- package/dist/src/kinds.js +1 -1
- package/dist/src/language.d.ts +17 -0
- package/dist/src/language.js +55 -0
- package/dist/src/layout.d.ts +69 -0
- package/dist/src/layout.js +200 -0
- package/dist/src/library.d.ts +9 -0
- package/dist/src/library.js +30 -3
- package/dist/src/limits.d.ts +84 -0
- package/dist/src/limits.js +287 -0
- package/dist/src/locale.d.ts +47 -0
- package/dist/src/locale.js +177 -0
- package/dist/src/mail.d.ts +88 -0
- package/dist/src/mail.js +247 -0
- package/dist/src/model-credential.d.ts +14 -0
- package/dist/src/model-credential.js +58 -0
- package/dist/src/model-probe.d.ts +3 -0
- package/dist/src/model-probe.js +1 -0
- package/dist/src/notify.d.ts +53 -7
- package/dist/src/notify.js +194 -126
- package/dist/src/openapi.js +5 -3
- package/dist/src/operator.d.ts +201 -0
- package/dist/src/operator.js +251 -0
- package/dist/src/paths.d.ts +28 -0
- package/dist/src/paths.js +55 -0
- package/dist/src/platform.d.ts +43 -3
- package/dist/src/platform.js +9 -1
- package/dist/src/provider-health.js +3 -2
- package/dist/src/providers.d.ts +481 -0
- package/dist/src/providers.js +1105 -6
- package/dist/src/refusal.d.ts +83 -0
- package/dist/src/refusal.js +281 -0
- package/dist/src/run-container.d.ts +149 -13
- package/dist/src/run-container.js +424 -43
- package/dist/src/runner.d.ts +177 -9
- package/dist/src/runner.js +1146 -154
- package/dist/src/runtime.d.ts +52 -0
- package/dist/src/runtime.js +493 -58
- package/dist/src/script-tools.d.ts +17 -2
- package/dist/src/script-tools.js +82 -10
- package/dist/src/secret-files.d.ts +6 -5
- package/dist/src/secret-files.js +24 -11
- package/dist/src/secret-health.d.ts +30 -6
- package/dist/src/secret-health.js +42 -20
- package/dist/src/secrets.d.ts +19 -0
- package/dist/src/secrets.js +155 -86
- package/dist/src/starter.d.ts +9 -0
- package/dist/src/starter.js +92 -11
- package/dist/src/step-exec.d.ts +72 -2
- package/dist/src/step-exec.js +431 -37
- package/dist/src/step-removal.d.ts +15 -0
- package/dist/src/step-removal.js +49 -0
- package/dist/src/storage.d.ts +23 -4
- package/dist/src/storage.js +125 -23
- package/dist/src/store.d.ts +222 -37
- package/dist/src/store.js +349 -62
- package/dist/src/subagents.d.ts +75 -0
- package/dist/src/subagents.js +149 -0
- package/dist/src/test-mode.d.ts +17 -2
- package/dist/src/test-mode.js +53 -13
- package/dist/src/tool-names.d.ts +13 -1
- package/dist/src/tool-names.js +38 -11
- package/dist/src/tool-programs.d.ts +26 -0
- package/dist/src/tool-programs.js +172 -0
- package/dist/src/tool-test.js +104 -12
- package/dist/src/translator.d.ts +8 -2
- package/dist/src/translator.js +69 -23
- package/dist/src/trigger-log.d.ts +1 -1
- package/dist/src/triggers.d.ts +3 -0
- package/dist/src/triggers.js +10 -2
- package/dist/src/untrusted-fetch.d.ts +11 -0
- package/dist/src/untrusted-fetch.js +77 -0
- package/dist/src/web-actions.d.ts +91 -0
- package/dist/src/web-actions.js +199 -0
- package/dist/src/webhook.d.ts +4 -0
- package/dist/src/webhook.js +39 -0
- package/index.ts +4 -0
- package/package.json +3 -3
- package/src/account.ts +149 -0
- package/src/api-tools.ts +32 -4
- package/src/browser-pod.ts +350 -0
- package/src/catalog.ts +2 -1
- package/src/clock.ts +149 -0
- package/src/completions.ts +17 -8
- package/src/confine.ts +218 -3
- package/src/container.ts +112 -5
- package/src/context-tools.ts +1 -1
- package/src/deploy.ts +137 -6
- package/src/egress.ts +13 -1
- package/src/eol.ts +7 -0
- package/src/evals.ts +14 -2
- package/src/flow-lint.ts +253 -1
- package/src/flow-patterns.ts +597 -0
- package/src/gitrepo.ts +48 -0
- package/src/host-env.ts +5 -0
- package/src/kinds.ts +1 -1
- package/src/language.ts +62 -0
- package/src/layout.ts +228 -0
- package/src/library.ts +37 -3
- package/src/limits.ts +290 -0
- package/src/locale.ts +217 -0
- package/src/mail.ts +318 -0
- package/src/model-credential.ts +71 -0
- package/src/model-probe.ts +4 -0
- package/src/notify.ts +189 -103
- package/src/openapi.ts +5 -3
- package/src/operator.ts +300 -0
- package/src/paths.ts +54 -0
- package/src/platform.ts +52 -3
- package/src/provider-health.ts +3 -2
- package/src/providers.ts +1368 -6
- package/src/refusal.ts +320 -0
- package/src/run-container.ts +460 -44
- package/src/runner.ts +1168 -155
- package/src/runtime.ts +487 -55
- package/src/script-tools.ts +81 -10
- package/src/secret-files.ts +22 -10
- package/src/secret-health.ts +64 -19
- package/src/secrets.ts +149 -87
- package/src/starter.ts +93 -11
- package/src/step-exec.ts +447 -38
- package/src/step-removal.ts +59 -0
- package/src/storage.ts +141 -22
- package/src/store.ts +506 -70
- package/src/subagents.ts +189 -0
- package/src/test-mode.ts +51 -11
- package/src/tool-names.ts +40 -11
- package/src/tool-programs.ts +179 -0
- package/src/tool-test.ts +110 -12
- package/src/translator.ts +72 -22
- package/src/trigger-log.ts +1 -0
- package/src/triggers.ts +10 -2
- package/src/untrusted-fetch.ts +88 -0
- package/src/web-actions.ts +237 -0
- package/src/webhook.ts +41 -0
- package/templates/blog-desk/agents/writer/agent.md +1 -1
- package/templates/hello/agents/curator/agent.md +4 -4
- 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
|
+
}
|
package/dist/src/library.d.ts
CHANGED
|
@@ -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
|
package/dist/src/library.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
+
};
|