@foldrun/core 0.4.0 → 0.5.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/dist/index.d.ts +3 -0
- package/dist/index.js +3 -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 +129 -0
- package/dist/src/browser-pod.js +284 -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/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 +6 -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/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 +245 -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 +403 -42
- package/dist/src/runner.d.ts +142 -9
- package/dist/src/runner.js +1027 -122
- package/dist/src/runtime.d.ts +52 -0
- package/dist/src/runtime.js +493 -58
- package/dist/src/script-tools.d.ts +3 -2
- package/dist/src/script-tools.js +35 -3
- 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 +61 -2
- package/dist/src/step-exec.js +414 -36
- 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 +218 -37
- package/dist/src/store.js +347 -60
- package/dist/src/subagents.d.ts +75 -0
- package/dist/src/subagents.js +149 -0
- 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 +3 -0
- package/package.json +3 -3
- package/src/account.ts +149 -0
- package/src/api-tools.ts +32 -4
- package/src/browser-pod.ts +313 -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/deploy.ts +137 -6
- package/src/egress.ts +13 -1
- package/src/eol.ts +7 -0
- package/src/evals.ts +11 -2
- package/src/flow-lint.ts +253 -1
- package/src/flow-patterns.ts +597 -0
- package/src/gitrepo.ts +48 -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 +316 -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 +439 -43
- package/src/runner.ts +1050 -122
- package/src/runtime.ts +487 -55
- package/src/script-tools.ts +38 -5
- 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 +429 -37
- package/src/step-removal.ts +59 -0
- package/src/storage.ts +141 -22
- package/src/store.ts +500 -68
- package/src/subagents.ts +189 -0
- 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/hello/agents/curator/agent.md +1 -1
- package/templates/hello/agents/notetaker/agent.md +1 -1
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { WEB_ACTIONS, type WebAction } from "./providers.ts";
|
|
2
|
+
export { WEB_ACTIONS, type WebAction };
|
|
3
|
+
/** What the browse action can return — its `mode`. Kept equal to the tool's own
|
|
4
|
+
* MODES by the platform's gallery test. */
|
|
5
|
+
export declare const BROWSE_MODES: readonly ["text", "markdown", "html", "links", "meta", "table", "network", "screenshot", "pdf", "capture", "aria", "map", "crawl", "console", "vitals", "a11y", "diff", "feed", "webmcp", "react"];
|
|
6
|
+
/** What the browse action can do in a page — its `actions` steps. Kept equal to
|
|
7
|
+
* the tool's own ACTIONS by the platform's gallery test. */
|
|
8
|
+
export declare const BROWSE_STEPS: readonly ["click", "dblclick", "rightclick", "hover", "check", "uncheck", "drag", "fill", "type", "press", "select", "wait", "goto", "screenshot", "pdf", "scroll", "paste", "upload", "download", "extract", "frame", "tab", "back", "forward", "reload", "focus", "clear", "highlight", "keyboard", "keydown", "keyup", "mouse", "wheel", "tap", "swipe", "clipboard", "eval", "get", "expect", "if", "mock", "unmock", "offline", "cookie", "localstorage", "sessionstorage", "viewport", "dialog", "solve", "pushstate", "insert", "webmcp", "login"];
|
|
9
|
+
export interface ActionSupport {
|
|
10
|
+
/** live: callable today. planned: agreed and not built — naming it is an
|
|
11
|
+
* error that says so, rather than a tool that does nothing. */
|
|
12
|
+
status: "live" | "planned";
|
|
13
|
+
/** How: which tool serves it, and for foldrun's own, with what. */
|
|
14
|
+
how: string;
|
|
15
|
+
/** Browse only. cdp: our code drives the browser, so every step and mode
|
|
16
|
+
* works unless listed in `lacks`. api: the vendor runs the steps, and
|
|
17
|
+
* `steps`/`modes` list what it has. */
|
|
18
|
+
via?: "cdp" | "api";
|
|
19
|
+
steps?: readonly string[];
|
|
20
|
+
modes?: readonly string[];
|
|
21
|
+
lacks?: readonly string[];
|
|
22
|
+
/** The vendor page the adapter was built from, and the day it was last
|
|
23
|
+
* matched to it (YYYY-MM-DD). Absent on foldrun's own. */
|
|
24
|
+
docs?: string;
|
|
25
|
+
checked?: string;
|
|
26
|
+
}
|
|
27
|
+
export interface WebProvider {
|
|
28
|
+
name: string;
|
|
29
|
+
title: string;
|
|
30
|
+
actions: Partial<Record<WebAction, ActionSupport>>;
|
|
31
|
+
}
|
|
32
|
+
/** Ours: every action, and none of them with a second model in it. */
|
|
33
|
+
export declare const FOLDRUN: WebProvider;
|
|
34
|
+
/** Every provider and what it can do, foldrun first. Built from the vendor
|
|
35
|
+
* lists so a vendor added there is here without a second edit. */
|
|
36
|
+
export declare function webProviders(): WebProvider[];
|
|
37
|
+
export declare function findWebProvider(name: string): WebProvider | undefined;
|
|
38
|
+
/** The live providers for an action, foldrun first — for "these do" lines. */
|
|
39
|
+
export declare function providersFor(action: WebAction): string[];
|
|
40
|
+
export type ActionChoice = {
|
|
41
|
+
provider: WebProvider;
|
|
42
|
+
support: ActionSupport;
|
|
43
|
+
error?: undefined;
|
|
44
|
+
} | {
|
|
45
|
+
provider?: undefined;
|
|
46
|
+
support?: undefined;
|
|
47
|
+
error: string;
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* Who does `action`. Unset is foldrun. A named provider must have the action
|
|
51
|
+
* live; anything else is an error naming the providers that do — the rule is
|
|
52
|
+
* "fix the file", never "we used ours instead".
|
|
53
|
+
*/
|
|
54
|
+
export declare function resolveWebAction(action: WebAction, name?: string | null): ActionChoice;
|
|
55
|
+
/** Can this browser run this step, or return this mode? */
|
|
56
|
+
export declare function browseSupports(support: ActionSupport, stepOrMode: string): boolean;
|
|
57
|
+
/** The browsers that can run a step or mode — for the error when one cannot. */
|
|
58
|
+
export declare function browsersFor(stepOrMode: string): string[];
|
|
59
|
+
/** Vendor actions whose adapter has not been matched to the vendor's docs
|
|
60
|
+
* within `days` — each as "provider action (checked …)". Empty is healthy.
|
|
61
|
+
* The test that calls this is the rule "integrations follow the latest
|
|
62
|
+
* docs" in a form CI can fail on. */
|
|
63
|
+
export declare function staleIntegrations(today: string, days?: number): string[];
|
|
64
|
+
/** The actions after search, fetch and browse. Those three are resolved by
|
|
65
|
+
* providers.ts, which also knows the model providers that search. */
|
|
66
|
+
export declare const NEWER_ACTIONS: readonly ["crawl", "map", "extract", "answer", "monitor"];
|
|
67
|
+
type NewerAction = (typeof NEWER_ACTIONS)[number];
|
|
68
|
+
export type ActionApiChoice = {
|
|
69
|
+
provider: null;
|
|
70
|
+
error?: undefined;
|
|
71
|
+
} | {
|
|
72
|
+
provider: string;
|
|
73
|
+
secret: string;
|
|
74
|
+
host: string;
|
|
75
|
+
error?: undefined;
|
|
76
|
+
} | {
|
|
77
|
+
provider: null;
|
|
78
|
+
error: string;
|
|
79
|
+
};
|
|
80
|
+
/**
|
|
81
|
+
* Who does one of the newer actions, and with which key: unset (or
|
|
82
|
+
* "foldrun") is ours and needs none; a name, or `{name, key: ${VAULT_NAME}}`
|
|
83
|
+
* for a key stored under another name, is that provider — if it has the
|
|
84
|
+
* action. The runner turns a provider into FOLDRUN_WEB_<ACTION>_VIA, the
|
|
85
|
+
* secret the step holds, and the one host that secret may reach.
|
|
86
|
+
*/
|
|
87
|
+
export declare function resolveActionApi(action: NewerAction, raw: unknown): ActionApiChoice;
|
|
88
|
+
/** Every newer action in the agent's web: block that names a provider unable
|
|
89
|
+
* to do it — the sentences resolveActionApi gives. Empty when all are unset
|
|
90
|
+
* or answerable. */
|
|
91
|
+
export declare function actionProblems(front: Record<string, unknown>): string[];
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
// The web, as actions. One question answered in one place: which provider
|
|
2
|
+
// can do which action, and what does foldrun do when none is named?
|
|
3
|
+
//
|
|
4
|
+
// An action is a call with a fixed input and a fixed output — search takes a
|
|
5
|
+
// query and returns URLs, fetch takes a URL and returns the page — whoever
|
|
6
|
+
// answers it. A provider is the list of actions it has an adapter for. foldrun
|
|
7
|
+
// is a provider like any other, and the one used when `web: {<action>: …}` is
|
|
8
|
+
// unset. A provider named for an action it cannot do is an error in `check`,
|
|
9
|
+
// never a quiet fall back to ours: a fallback would bill and behave
|
|
10
|
+
// differently from what the file says.
|
|
11
|
+
//
|
|
12
|
+
// Browse has a second level: inside a page, the steps and modes of
|
|
13
|
+
// the browse action. A browser reached over CDP is driven by our own code, so it
|
|
14
|
+
// takes every step and mode unless it is listed as lacking one; a vendor
|
|
15
|
+
// that runs the steps itself would list the few it has.
|
|
16
|
+
//
|
|
17
|
+
// The vendor facts (endpoint, key, docs page, the date the adapter was last
|
|
18
|
+
// matched to those docs) stay on SEARCH_APIS, FETCH_APIS and BROWSER_APIS in
|
|
19
|
+
// providers.ts; this file reads them rather than restating them.
|
|
20
|
+
import { SEARCH_APIS, FETCH_APIS, BROWSER_APIS, ACTION_APIS, WEB_ACTIONS, webConfig } from "./providers.js";
|
|
21
|
+
export { WEB_ACTIONS };
|
|
22
|
+
/** What the browse action can return — its `mode`. Kept equal to the tool's own
|
|
23
|
+
* MODES by the platform's gallery test. */
|
|
24
|
+
export const BROWSE_MODES = [
|
|
25
|
+
"text", "markdown", "html", "links", "meta", "table", "network", "screenshot", "pdf", "capture",
|
|
26
|
+
"aria", "map", "crawl", "console", "vitals", "a11y", "diff", "feed", "webmcp", "react",
|
|
27
|
+
];
|
|
28
|
+
/** What the browse action can do in a page — its `actions` steps. Kept equal to
|
|
29
|
+
* the tool's own ACTIONS by the platform's gallery test. */
|
|
30
|
+
export const BROWSE_STEPS = [
|
|
31
|
+
"click", "dblclick", "rightclick", "hover", "check", "uncheck", "drag", "fill", "type", "press", "select",
|
|
32
|
+
"wait", "goto", "screenshot", "pdf", "scroll", "paste", "upload", "download", "extract", "frame", "tab",
|
|
33
|
+
"back", "forward", "reload", "focus", "clear", "highlight", "keyboard", "keydown", "keyup", "mouse", "wheel",
|
|
34
|
+
"tap", "swipe", "clipboard", "eval", "get", "expect", "if", "mock", "unmock", "offline", "cookie",
|
|
35
|
+
"localstorage", "sessionstorage", "viewport", "dialog", "solve", "pushstate", "insert", "webmcp", "login",
|
|
36
|
+
];
|
|
37
|
+
/** Ours: every action, and none of them with a second model in it. */
|
|
38
|
+
export const FOLDRUN = {
|
|
39
|
+
name: "foldrun",
|
|
40
|
+
title: "foldrun",
|
|
41
|
+
actions: {
|
|
42
|
+
search: { status: "live", how: "the account's own SearXNG" },
|
|
43
|
+
fetch: { status: "live", how: "one plain HTTP request in the sandbox" },
|
|
44
|
+
browse: { status: "live", how: "the account's browser pod", via: "cdp" },
|
|
45
|
+
crawl: { status: "live", how: "plain HTTP, same site, robots.txt honoured" },
|
|
46
|
+
map: { status: "live", how: "the site's sitemaps, else its links" },
|
|
47
|
+
extract: { status: "live", how: "selectors, or the page's own JSON-LD and OpenGraph — no model" },
|
|
48
|
+
answer: { status: "live", how: "search, read the top pages, rank the passages that answer — no model" },
|
|
49
|
+
monitor: { status: "live", how: "a page's lines or a query's results, against the last call, in state/" },
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
/** Every provider and what it can do, foldrun first. Built from the vendor
|
|
53
|
+
* lists so a vendor added there is here without a second edit. */
|
|
54
|
+
export function webProviders() {
|
|
55
|
+
const byName = new Map();
|
|
56
|
+
const add = (f, action, support) => {
|
|
57
|
+
const p = byName.get(f.name) ?? { name: f.name, title: f.title, actions: {} };
|
|
58
|
+
p.actions[action] = { ...support, ...(f.docs ? { docs: f.docs } : {}), ...(f.checked ? { checked: f.checked } : {}) };
|
|
59
|
+
byName.set(f.name, p);
|
|
60
|
+
};
|
|
61
|
+
for (const a of SEARCH_APIS)
|
|
62
|
+
add(a, "search", { status: "live", how: a.title });
|
|
63
|
+
for (const a of FETCH_APIS)
|
|
64
|
+
add(a, "fetch", { status: "live", how: a.title });
|
|
65
|
+
for (const a of BROWSER_APIS) {
|
|
66
|
+
add(a, "browse", { status: "live", how: a.title, via: "cdp", ...(a.lacks?.length ? { lacks: a.lacks } : {}) });
|
|
67
|
+
}
|
|
68
|
+
for (const [action, apis] of Object.entries(ACTION_APIS)) {
|
|
69
|
+
for (const a of apis)
|
|
70
|
+
add(a, action, { status: "live", how: a.title });
|
|
71
|
+
}
|
|
72
|
+
return [FOLDRUN, ...byName.values()];
|
|
73
|
+
}
|
|
74
|
+
/** The names a provider answers to, for lookups. */
|
|
75
|
+
function aliasesOf(name) {
|
|
76
|
+
const all = [...SEARCH_APIS, ...FETCH_APIS, ...BROWSER_APIS, ...Object.values(ACTION_APIS).flat()];
|
|
77
|
+
return all.filter((a) => a.name === name).flatMap((a) => a.aliases ?? []);
|
|
78
|
+
}
|
|
79
|
+
export function findWebProvider(name) {
|
|
80
|
+
const key = name.trim().toLowerCase();
|
|
81
|
+
if (key === "" || key === "ours")
|
|
82
|
+
return FOLDRUN;
|
|
83
|
+
return webProviders().find((p) => p.name === key || aliasesOf(p.name).includes(key));
|
|
84
|
+
}
|
|
85
|
+
/** The live providers for an action, foldrun first — for "these do" lines. */
|
|
86
|
+
export function providersFor(action) {
|
|
87
|
+
return webProviders().filter((p) => p.actions[action]?.status === "live").map((p) => p.name);
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Who does `action`. Unset is foldrun. A named provider must have the action
|
|
91
|
+
* live; anything else is an error naming the providers that do — the rule is
|
|
92
|
+
* "fix the file", never "we used ours instead".
|
|
93
|
+
*/
|
|
94
|
+
export function resolveWebAction(action, name) {
|
|
95
|
+
const field = `web.${action}`;
|
|
96
|
+
const p = name == null ? FOLDRUN : findWebProvider(name);
|
|
97
|
+
if (!p) {
|
|
98
|
+
return { error: `${field}: ${name} — no provider by that name. These ${action}: ${providersFor(action).join(", ")}.` };
|
|
99
|
+
}
|
|
100
|
+
const s = p.actions[action];
|
|
101
|
+
if (!s) {
|
|
102
|
+
const live = providersFor(action);
|
|
103
|
+
return {
|
|
104
|
+
error: `${field}: ${p.name} does not ${action} here.` +
|
|
105
|
+
(live.length ? ` These do: ${live.join(", ")}.` : " No provider has it wired yet.") +
|
|
106
|
+
(FOLDRUN.actions[action]?.status === "live" ? " Unset uses foldrun." : ""),
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
if (s.status === "planned") {
|
|
110
|
+
const others = providersFor(action).filter((n) => n !== p.name);
|
|
111
|
+
return {
|
|
112
|
+
error: `${field}: ${p.title}'s own ${action} is not built yet` +
|
|
113
|
+
(others.length ? ` — name a provider that has it: ${others.join(", ")}.` : " and no provider has it wired either."),
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
return { provider: p, support: s };
|
|
117
|
+
}
|
|
118
|
+
/** Can this browser run this step, or return this mode? */
|
|
119
|
+
export function browseSupports(support, stepOrMode) {
|
|
120
|
+
if (support.via === "cdp")
|
|
121
|
+
return !(support.lacks ?? []).includes(stepOrMode);
|
|
122
|
+
return (support.steps ?? []).includes(stepOrMode) || (support.modes ?? []).includes(stepOrMode);
|
|
123
|
+
}
|
|
124
|
+
/** The browsers that can run a step or mode — for the error when one cannot. */
|
|
125
|
+
export function browsersFor(stepOrMode) {
|
|
126
|
+
return webProviders()
|
|
127
|
+
.filter((p) => p.actions.browse?.status === "live" && browseSupports(p.actions.browse, stepOrMode))
|
|
128
|
+
.map((p) => p.name);
|
|
129
|
+
}
|
|
130
|
+
/** Vendor actions whose adapter has not been matched to the vendor's docs
|
|
131
|
+
* within `days` — each as "provider action (checked …)". Empty is healthy.
|
|
132
|
+
* The test that calls this is the rule "integrations follow the latest
|
|
133
|
+
* docs" in a form CI can fail on. */
|
|
134
|
+
export function staleIntegrations(today, days = 90) {
|
|
135
|
+
const out = [];
|
|
136
|
+
const limit = Date.parse(today) - days * 86_400_000;
|
|
137
|
+
for (const p of webProviders()) {
|
|
138
|
+
if (p === FOLDRUN)
|
|
139
|
+
continue;
|
|
140
|
+
for (const [action, s] of Object.entries(p.actions)) {
|
|
141
|
+
if (!s || s.status !== "live")
|
|
142
|
+
continue;
|
|
143
|
+
if (!s.docs || !s.checked)
|
|
144
|
+
out.push(`${p.name} ${action} (no docs page or check date recorded)`);
|
|
145
|
+
else if (Date.parse(s.checked) < limit)
|
|
146
|
+
out.push(`${p.name} ${action} (checked ${s.checked})`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return out;
|
|
150
|
+
}
|
|
151
|
+
/** The actions after search, fetch and browse. Those three are resolved by
|
|
152
|
+
* providers.ts, which also knows the model providers that search. */
|
|
153
|
+
export const NEWER_ACTIONS = ["crawl", "map", "extract", "answer", "monitor"];
|
|
154
|
+
/**
|
|
155
|
+
* Who does one of the newer actions, and with which key: unset (or
|
|
156
|
+
* "foldrun") is ours and needs none; a name, or `{name, key: ${VAULT_NAME}}`
|
|
157
|
+
* for a key stored under another name, is that provider — if it has the
|
|
158
|
+
* action. The runner turns a provider into FOLDRUN_WEB_<ACTION>_VIA, the
|
|
159
|
+
* secret the step holds, and the one host that secret may reach.
|
|
160
|
+
*/
|
|
161
|
+
export function resolveActionApi(action, raw) {
|
|
162
|
+
const field = `web.${action}`;
|
|
163
|
+
if (raw === undefined || raw === null || raw === "")
|
|
164
|
+
return { provider: null };
|
|
165
|
+
let name;
|
|
166
|
+
let secret;
|
|
167
|
+
if (typeof raw === "string")
|
|
168
|
+
name = raw;
|
|
169
|
+
else if (typeof raw === "object" && !Array.isArray(raw) && typeof raw.name === "string") {
|
|
170
|
+
name = raw.name;
|
|
171
|
+
const key = raw.key;
|
|
172
|
+
if (key !== undefined) {
|
|
173
|
+
const m = typeof key === "string" ? /^\$\{([A-Z][A-Z0-9_]*)\}$/.exec(key.trim()) : null;
|
|
174
|
+
if (!m)
|
|
175
|
+
return { provider: null, error: `${field}.key must be a \${NAME} reference to a secret in the vault, not the key itself.` };
|
|
176
|
+
secret = m[1];
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
return { provider: null, error: `${field}: takes a provider name, or a block with name: (and key: for your own vault name).` };
|
|
181
|
+
}
|
|
182
|
+
const choice = resolveWebAction(action, name);
|
|
183
|
+
if (choice.error !== undefined)
|
|
184
|
+
return { provider: null, error: choice.error };
|
|
185
|
+
if (choice.provider.name === "foldrun")
|
|
186
|
+
return { provider: null };
|
|
187
|
+
const api = ACTION_APIS[action].find((a) => a.name === choice.provider.name);
|
|
188
|
+
return { provider: api.name, secret: secret ?? api.secret, host: api.host };
|
|
189
|
+
}
|
|
190
|
+
/** Every newer action in the agent's web: block that names a provider unable
|
|
191
|
+
* to do it — the sentences resolveActionApi gives. Empty when all are unset
|
|
192
|
+
* or answerable. */
|
|
193
|
+
export function actionProblems(front) {
|
|
194
|
+
const { raw } = webConfig(front);
|
|
195
|
+
return NEWER_ACTIONS.flatMap((a) => {
|
|
196
|
+
const e = resolveActionApi(a, raw[a]).error;
|
|
197
|
+
return e ? [e] : [];
|
|
198
|
+
});
|
|
199
|
+
}
|
package/dist/src/webhook.d.ts
CHANGED
|
@@ -54,6 +54,10 @@ export interface HookDelivery {
|
|
|
54
54
|
runId?: string;
|
|
55
55
|
bytes?: number;
|
|
56
56
|
detail?: string;
|
|
57
|
+
/** A run of refusals, collapsed: how many, and when the first came.
|
|
58
|
+
* `t` is the latest. Absent means one. */
|
|
59
|
+
count?: number;
|
|
60
|
+
first?: string;
|
|
57
61
|
}
|
|
58
62
|
export declare function recordDelivery(tenant: string, workspace: string, delivery: HookDelivery): void;
|
|
59
63
|
export declare function readDeliveries(tenant: string, workspace: string, limit?: number): HookDelivery[];
|
package/dist/src/webhook.js
CHANGED
|
@@ -103,6 +103,10 @@ export function publicUrl() {
|
|
|
103
103
|
return null;
|
|
104
104
|
return raw.replace(/\/+$/, "");
|
|
105
105
|
}
|
|
106
|
+
/** What anyone can produce without the token or the signing secret. A run
|
|
107
|
+
* of these collapses into one line, so a stranger's flood costs the log a
|
|
108
|
+
* single entry and can never push an owner's real deliveries out of it. */
|
|
109
|
+
const UNAUTHENTICATED = ["invalid-token", "invalid-signature"];
|
|
106
110
|
function deliveriesFile(tenant, workspace) {
|
|
107
111
|
return path.join(workspaceDir(tenant, workspace), "hook-deliveries.jsonl");
|
|
108
112
|
}
|
|
@@ -111,6 +115,8 @@ export function recordDelivery(tenant, workspace, delivery) {
|
|
|
111
115
|
const file = deliveriesFile(tenant, workspace);
|
|
112
116
|
if (!fs.existsSync(path.dirname(file)))
|
|
113
117
|
return; // no workspace, nothing to log against
|
|
118
|
+
if (UNAUTHENTICATED.includes(delivery.outcome) && collapseInto(file, delivery))
|
|
119
|
+
return;
|
|
114
120
|
fs.appendFileSync(file, JSON.stringify(delivery) + "\n");
|
|
115
121
|
// Bounded: compact to the newest KEEP when it doubles. Amortised cheap,
|
|
116
122
|
// and a log that grows forever is a disk-full incident with a delay.
|
|
@@ -126,6 +132,39 @@ export function recordDelivery(tenant, workspace, delivery) {
|
|
|
126
132
|
// compaction is best-effort; the append already landed
|
|
127
133
|
}
|
|
128
134
|
}
|
|
135
|
+
/** Fold a refusal into the log's last line when that line is a refusal
|
|
136
|
+
* too. The flow name is not part of the match: it comes from the URL, so
|
|
137
|
+
* a flood that varies it would otherwise never collapse. True when folded. */
|
|
138
|
+
function collapseInto(file, delivery) {
|
|
139
|
+
let text;
|
|
140
|
+
try {
|
|
141
|
+
text = fs.readFileSync(file, "utf8");
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
const end = text.trimEnd();
|
|
147
|
+
const cut = end.lastIndexOf("\n") + 1;
|
|
148
|
+
let last;
|
|
149
|
+
try {
|
|
150
|
+
last = JSON.parse(end.slice(cut));
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
if (!UNAUTHENTICATED.includes(last.outcome))
|
|
156
|
+
return false;
|
|
157
|
+
const merged = {
|
|
158
|
+
...delivery,
|
|
159
|
+
count: (last.count ?? 1) + 1,
|
|
160
|
+
first: last.first ?? last.t,
|
|
161
|
+
...(last.detail === "more than one flow name" || last.flow !== delivery.flow ? { detail: "more than one flow name" } : {}),
|
|
162
|
+
};
|
|
163
|
+
const tmp = `${file}.tmp`;
|
|
164
|
+
fs.writeFileSync(tmp, end.slice(0, cut) + JSON.stringify(merged) + "\n");
|
|
165
|
+
fs.renameSync(tmp, file);
|
|
166
|
+
return true;
|
|
167
|
+
}
|
|
129
168
|
export function readDeliveries(tenant, workspace, limit = 20) {
|
|
130
169
|
const file = deliveriesFile(tenant, workspace);
|
|
131
170
|
if (!fs.existsSync(file))
|
package/index.ts
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
// database, previews, share links, tenancy — lives in its own private package
|
|
11
11
|
// and plugs in through platform.ts. Nothing here imports it.
|
|
12
12
|
|
|
13
|
+
export * from "./src/layout.ts";
|
|
13
14
|
export * from "./src/paths.ts";
|
|
14
15
|
export * from "./src/store.ts";
|
|
15
16
|
export * from "./src/starter.ts";
|
|
@@ -22,6 +23,7 @@ export * from "./src/model-probe.ts";
|
|
|
22
23
|
export * from "./src/providers.ts";
|
|
23
24
|
export * from "./src/translator.ts";
|
|
24
25
|
export * from "./src/arrange.ts";
|
|
26
|
+
export * from "./src/flow-patterns.ts";
|
|
25
27
|
export * from "./src/completions.ts";
|
|
26
28
|
export * from "./src/confine.ts";
|
|
27
29
|
export * from "./src/platform.ts";
|
|
@@ -40,5 +42,6 @@ export * from "./src/notify.ts";
|
|
|
40
42
|
export * from "./src/webhook.ts";
|
|
41
43
|
export * from "./src/approvals.ts";
|
|
42
44
|
export * from "./src/deploy.ts";
|
|
45
|
+
export * from "./src/account.ts";
|
|
43
46
|
export * from "./src/tar.ts";
|
|
44
47
|
export * from "./src/git.ts";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@foldrun/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Agents are just folders. The format, the runner, the checks.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
],
|
|
27
27
|
"scripts": {
|
|
28
28
|
"build": "tsc -p tsconfig.json",
|
|
29
|
-
"prepare": "npm run build",
|
|
29
|
+
"prepare": "npm run build && (test ! -f .githooks/install.sh || sh .githooks/install.sh)",
|
|
30
30
|
"prepublishOnly": "npm run build",
|
|
31
31
|
"test": "FOLDRUN_DATA=$(mktemp -d /tmp/foldrun-test-XXXXXX) node --test *.test.ts $(ls tests/*.test.ts | grep -v e2e)",
|
|
32
32
|
"container": "FOLDRUN_CONTAINER_E2E=1 node scripts/ts-test.mjs tests/container-e2e.test.ts --test-timeout=600000",
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"node": ">=22"
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"@anthropic-ai/claude-agent-sdk": "^0.3.
|
|
39
|
+
"@anthropic-ai/claude-agent-sdk": "^0.3.285",
|
|
40
40
|
"gray-matter": "^4.0.3",
|
|
41
41
|
"zod": "4.4.3"
|
|
42
42
|
},
|
package/src/account.ts
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
// An account folder, read as the thing a deploy pushes.
|
|
2
|
+
//
|
|
3
|
+
// `readTree` answers the same question for one workspace: what, of the files
|
|
4
|
+
// on disk, is source? This is that question one scope up — the account's own
|
|
5
|
+
// AGENTS.md, the shared `library/`, and every workspace under `workspaces/` —
|
|
6
|
+
// so `foldrun deploy` at an account root has one list to walk and one shape to
|
|
7
|
+
// report, rather than three special cases written at the call site.
|
|
8
|
+
//
|
|
9
|
+
// Nothing here talks to a platform. It reads a directory and says what is in
|
|
10
|
+
// it; who gets sent where is the CLI's business, because the endpoints differ
|
|
11
|
+
// per part (a workspace deploys whole, a library file writes one at a time)
|
|
12
|
+
// and that asymmetry belongs next to the fetch calls, not in here.
|
|
13
|
+
|
|
14
|
+
import fs from "node:fs";
|
|
15
|
+
import path from "node:path";
|
|
16
|
+
import { readTree } from "./deploy.ts";
|
|
17
|
+
import { LIBRARY_KINDS, type LibraryKind } from "./library.ts";
|
|
18
|
+
import { detectLayout, listWorkspaceDirs, type Layout } from "./layout.ts";
|
|
19
|
+
import type { DeployFile } from "./store.ts";
|
|
20
|
+
|
|
21
|
+
export interface AccountWorkspace {
|
|
22
|
+
name: string;
|
|
23
|
+
dir: string;
|
|
24
|
+
files: DeployFile[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface LibraryFile {
|
|
28
|
+
kind: LibraryKind;
|
|
29
|
+
/** Relative to the kind's directory — what the library API calls `path`. */
|
|
30
|
+
path: string;
|
|
31
|
+
content: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface AccountTree {
|
|
35
|
+
root: string;
|
|
36
|
+
/** The account-scope AGENTS.md, or null when there is none. */
|
|
37
|
+
agentsMd: string | null;
|
|
38
|
+
library: LibraryFile[];
|
|
39
|
+
workspaces: AccountWorkspace[];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Anything that is bookkeeping rather than source, at either scope. */
|
|
43
|
+
const NOT_SOURCE = new Set([".foldrun", ".git", "node_modules", "runs", "outputs", ".DS_Store"]);
|
|
44
|
+
|
|
45
|
+
function walkFiles(root: string, rel = ""): { path: string; content: string }[] {
|
|
46
|
+
const out: { path: string; content: string }[] = [];
|
|
47
|
+
let entries: string[];
|
|
48
|
+
try {
|
|
49
|
+
entries = fs.readdirSync(root).sort();
|
|
50
|
+
} catch {
|
|
51
|
+
return out;
|
|
52
|
+
}
|
|
53
|
+
for (const entry of entries) {
|
|
54
|
+
if (NOT_SOURCE.has(entry)) continue;
|
|
55
|
+
const full = path.join(root, entry);
|
|
56
|
+
const next = rel ? `${rel}/${entry}` : entry;
|
|
57
|
+
try {
|
|
58
|
+
if (fs.lstatSync(full).isSymbolicLink()) continue;
|
|
59
|
+
if (fs.statSync(full).isDirectory()) out.push(...walkFiles(full, next));
|
|
60
|
+
else out.push({ path: next, content: fs.readFileSync(full, "utf8") });
|
|
61
|
+
} catch {
|
|
62
|
+
/* unreadable: not source */
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return out;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** The shared library under an account root, flattened to one list. */
|
|
69
|
+
export function readLibraryTree(accountRoot: string): LibraryFile[] {
|
|
70
|
+
const out: LibraryFile[] = [];
|
|
71
|
+
for (const kind of LIBRARY_KINDS) {
|
|
72
|
+
const dir = path.join(accountRoot, "library", kind);
|
|
73
|
+
for (const f of walkFiles(dir)) {
|
|
74
|
+
// `.gitkeep` and friends keep an empty library directory in git and are
|
|
75
|
+
// not library entries: the write endpoint refuses a path with no known
|
|
76
|
+
// extension, so shipping one would fail the deploy over a placeholder.
|
|
77
|
+
if (f.path.split("/").some((seg) => seg.startsWith("."))) continue;
|
|
78
|
+
out.push({ kind, path: f.path, content: f.content });
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return out;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Everything an account folder would push: its AGENTS.md, its library, and
|
|
86
|
+
* every workspace under it.
|
|
87
|
+
*
|
|
88
|
+
* `only` narrows it to one workspace — `foldrun deploy <name>` — and still
|
|
89
|
+
* carries the account scope, because a workspace deployed without the library
|
|
90
|
+
* its agents name is a workspace that fails on its first run.
|
|
91
|
+
*/
|
|
92
|
+
export function readAccountTree(accountRoot: string, only?: string): AccountTree {
|
|
93
|
+
const root = path.resolve(accountRoot);
|
|
94
|
+
const agentsMdFile = path.join(root, "AGENTS.md");
|
|
95
|
+
let agentsMd: string | null = null;
|
|
96
|
+
try {
|
|
97
|
+
agentsMd = fs.readFileSync(agentsMdFile, "utf8");
|
|
98
|
+
} catch {
|
|
99
|
+
/* an account with no AGENTS.md pushes none */
|
|
100
|
+
}
|
|
101
|
+
const names = listWorkspaceDirs(root).filter((n) => (only ? n === only : true));
|
|
102
|
+
if (only && names.length === 0) {
|
|
103
|
+
throw new Error(`no workspace "${only}" in ${root} — have: ${listWorkspaceDirs(root).join(", ") || "none"}`);
|
|
104
|
+
}
|
|
105
|
+
const wsRoot = path.join(root, "workspaces");
|
|
106
|
+
const legacy = path.join(root, "projects");
|
|
107
|
+
const base = fs.existsSync(wsRoot) ? wsRoot : legacy;
|
|
108
|
+
return {
|
|
109
|
+
root,
|
|
110
|
+
agentsMd,
|
|
111
|
+
library: readLibraryTree(root),
|
|
112
|
+
workspaces: names.map((name) => {
|
|
113
|
+
const dir = path.join(base, name);
|
|
114
|
+
return { name, dir, files: readTree(dir) };
|
|
115
|
+
}),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* The account tree for wherever you are standing — an account root, a
|
|
121
|
+
* workspace inside one, or a flat workspace.
|
|
122
|
+
*
|
|
123
|
+
* A flat workspace is reported as an account of one whose root is its parent,
|
|
124
|
+
* which is exactly what `accountDir` and `libraryDir` already believe about
|
|
125
|
+
* it. So the deploy path has no branch for the old layout: it pushes one
|
|
126
|
+
* workspace and whatever library sits beside it, which is what it did before.
|
|
127
|
+
*/
|
|
128
|
+
export function accountTreeFrom(from: string, only?: string): { layout: Layout; tree: AccountTree } {
|
|
129
|
+
const layout = detectLayout(from);
|
|
130
|
+
if (layout.kind === "flat" || layout.kind === "empty") {
|
|
131
|
+
const dir = layout.workspaceDir!;
|
|
132
|
+
const name = path.basename(dir);
|
|
133
|
+
if (only && only !== name) throw new Error(`${dir} is a single workspace called "${name}", not "${only}"`);
|
|
134
|
+
return {
|
|
135
|
+
layout,
|
|
136
|
+
tree: {
|
|
137
|
+
root: layout.accountRoot,
|
|
138
|
+
// A flat workspace's AGENTS.md belongs to the workspace, not the
|
|
139
|
+
// account — pushing it as account scope would apply one desk's rules
|
|
140
|
+
// to every other desk in the installation.
|
|
141
|
+
agentsMd: null,
|
|
142
|
+
library: readLibraryTree(layout.accountRoot),
|
|
143
|
+
workspaces: [{ name, dir, files: readTree(dir) }],
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
const only2 = only ?? (layout.kind === "workspace-in-account" ? layout.workspace! : undefined);
|
|
148
|
+
return { layout, tree: readAccountTree(layout.accountRoot, only2) };
|
|
149
|
+
}
|
package/src/api-tools.ts
CHANGED
|
@@ -101,6 +101,14 @@ export interface ApiToolResult {
|
|
|
101
101
|
server: ReturnType<typeof createSdkMcpServer> | null;
|
|
102
102
|
/** Tool names to allow, e.g. mcp__foldrun_apis__call_google_ads. */
|
|
103
103
|
toolNames: string[];
|
|
104
|
+
/** SDK tool name → the API it was built for, recorded as each tool is
|
|
105
|
+
* made: `crm`'s operation `admin_list` is `crm_admin_list` beside an API
|
|
106
|
+
* named `crm_admin`, so the name alone cannot say whose it is. */
|
|
107
|
+
owners: Record<string, string>;
|
|
108
|
+
/** SDK tool name → the HTTP method of a typed operation. A generic
|
|
109
|
+
* call_<api> carries its method in each call's input instead. Read by the
|
|
110
|
+
* browser-pod fallback (browser-pod.ts) to tell a GET from a write. */
|
|
111
|
+
methods: Record<string, string>;
|
|
104
112
|
/** Secrets referenced but not set — surfaced as run warnings. */
|
|
105
113
|
missingSecrets: string[];
|
|
106
114
|
/** Human-readable lines describing each API, appended to the system prompt. */
|
|
@@ -318,7 +326,7 @@ export function buildApiTools(
|
|
|
318
326
|
resolved?: { env: Record<string, string | undefined>; missing: string[] },
|
|
319
327
|
): ApiToolResult {
|
|
320
328
|
if (apis.length === 0) {
|
|
321
|
-
return { server: null, toolNames: [], missingSecrets: [], promptLines: [], drainLog: () => [] };
|
|
329
|
+
return { server: null, toolNames: [], owners: {}, methods: {}, missingSecrets: [], promptLines: [], drainLog: () => [] };
|
|
322
330
|
}
|
|
323
331
|
|
|
324
332
|
const neededSecrets = [...new Set(apis.flatMap(secretsUsedByApi))];
|
|
@@ -337,6 +345,8 @@ export function buildApiTools(
|
|
|
337
345
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- the SDK types its own list this way
|
|
338
346
|
const tools: SdkMcpToolDefinition<any>[] = [];
|
|
339
347
|
const toolNames: string[] = [];
|
|
348
|
+
const owners: Record<string, string> = {};
|
|
349
|
+
const methods: Record<string, string> = {};
|
|
340
350
|
const promptLines: string[] = [];
|
|
341
351
|
|
|
342
352
|
for (const api of apis) {
|
|
@@ -344,18 +354,34 @@ export function buildApiTools(
|
|
|
344
354
|
// this process. Two steps on two workers each get their own.
|
|
345
355
|
const bucket = api.rate ? new TokenBucket(api.rate.count, api.rate.perSec) : null;
|
|
346
356
|
const ops = api.resolvedOperations ?? [];
|
|
347
|
-
// An explicit `operations:` list means "only these": no escape hatch
|
|
348
|
-
|
|
357
|
+
// An explicit `operations:` list means "only these": no escape hatch,
|
|
358
|
+
// ever. It fails closed. If the document did not load, or yielded none of
|
|
359
|
+
// the listed operations, the API gets no tools this run rather than the
|
|
360
|
+
// generic one: an allowlist that opens when a vendor renames an
|
|
361
|
+
// operation or a URL moves is not an allowlist.
|
|
362
|
+
const allowlisted = !!api.operations?.length;
|
|
363
|
+
const withGeneric = !allowlisted;
|
|
364
|
+
if (allowlisted && !ops.length) {
|
|
365
|
+
promptLines.push(
|
|
366
|
+
`- **${api.name}** — unavailable this run: its \`operations:\` allowlist resolved no operations, ` +
|
|
367
|
+
`so no tool is exposed. Report it as blocked; do not reach this API another way.`,
|
|
368
|
+
);
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
349
371
|
|
|
350
372
|
const typedNames: string[] = [];
|
|
351
373
|
for (const op of ops) {
|
|
352
374
|
tools.push(typedTool(api, op, env, bucket, log));
|
|
353
375
|
typedNames.push(typedToolName(api.name, op.id));
|
|
376
|
+
methods[`mcp__foldrun_apis__${typedToolName(api.name, op.id)}`] = String(op.method ?? "").toUpperCase();
|
|
354
377
|
}
|
|
355
378
|
if (withGeneric) tools.push(genericTool(api, env, bucket, log));
|
|
356
379
|
|
|
357
380
|
const names = [...typedNames, ...(withGeneric ? [`call_${api.name}`] : [])];
|
|
358
|
-
|
|
381
|
+
for (const n of names) {
|
|
382
|
+
toolNames.push(`mcp__foldrun_apis__${n}`);
|
|
383
|
+
owners[`mcp__foldrun_apis__${n}`] = api.name;
|
|
384
|
+
}
|
|
359
385
|
|
|
360
386
|
const rateLine = api.rate ? ` Rate limit ${api.rate.count}/${Math.round(api.rate.count / api.rate.perSec)}s — over it, calls wait.` : "";
|
|
361
387
|
if (ops.length) {
|
|
@@ -382,6 +408,8 @@ export function buildApiTools(
|
|
|
382
408
|
return {
|
|
383
409
|
server,
|
|
384
410
|
toolNames,
|
|
411
|
+
owners,
|
|
412
|
+
methods,
|
|
385
413
|
missingSecrets: missing,
|
|
386
414
|
promptLines,
|
|
387
415
|
drainLog: () => log.splice(0, log.length),
|