foldrun 0.9.0 → 1.0.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/bin/foldrun.mjs +21 -7
- package/docs/agents.md +2 -1
- package/docs/api-usage.md +16 -2
- package/docs/api.md +4 -2
- package/docs/backups-and-restore.md +35 -66
- package/docs/budgets-and-billing.md +1 -1
- package/docs/cli.md +13 -5
- package/docs/hooks.md +139 -0
- package/docs/notifications.md +7 -1
- package/docs/security.md +14 -0
- package/docs/tools.md +30 -5
- package/docs/versions.md +39 -2
- package/package.json +3 -3
- package/src/commands.mjs +481 -17
- package/src/guide.mjs +1 -1
- package/src/terminal.mjs +85 -0
package/src/commands.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// CLI commands. Kept separate from bin/ so the environment is set before the
|
|
2
2
|
// core is imported — single-workspace mode is read at module load.
|
|
3
3
|
|
|
4
|
+
import { writeExact } from "./terminal.mjs";
|
|
4
5
|
import dns from "node:dns";
|
|
5
6
|
import fs from "node:fs";
|
|
6
7
|
import crypto from "node:crypto";
|
|
@@ -710,8 +711,10 @@ async function platformLibrary(flags) {
|
|
|
710
711
|
return new Set((body.entries ?? []).map((e) => e.name).filter(Boolean));
|
|
711
712
|
};
|
|
712
713
|
try {
|
|
713
|
-
|
|
714
|
-
|
|
714
|
+
// Hooks are newer than the library route on some platforms: a platform
|
|
715
|
+
// that does not list them leaves the set empty rather than failing check.
|
|
716
|
+
const [tools, skills, hooks] = await Promise.all([names("tools"), names("skills"), names("hooks").catch(() => new Set())]);
|
|
717
|
+
return { url, tools, skills, hooks, warning: null };
|
|
715
718
|
} catch (err) {
|
|
716
719
|
return { url, tools: new Set(), skills: new Set(), warning: `could not read the library on ${url} (${err instanceof Error ? err.message : err}) — tools defined there will be reported missing` };
|
|
717
720
|
}
|
|
@@ -772,6 +775,11 @@ async function checkAccount(layout, flags) {
|
|
|
772
775
|
const problems = [];
|
|
773
776
|
const note = (level, where, message) => problems.push({ level, where, message });
|
|
774
777
|
validateSkills(path.join(layout.accountRoot, "library"), note);
|
|
778
|
+
const hc = await hooksCore();
|
|
779
|
+
for (const p of hc ? hc.checkHooksDir(path.join(layout.accountRoot, "library", "hooks"), "account") : []) {
|
|
780
|
+
const m = /^(hooks\/[^/:]+(?:\/hook\.md)?): (.*)$/s.exec(p);
|
|
781
|
+
note("error", m ? m[1] : "hooks/", m ? m[2] : p);
|
|
782
|
+
}
|
|
775
783
|
const counts = LIBRARY_KINDS.map((k) => ({ kind: k, n: files.filter((f) => f.kind === k).length })).filter((x) => x.n > 0);
|
|
776
784
|
console.log(`\n ${c.bold("library")} ${c.dim(counts.length ? counts.map((x) => `${x.n} ${x.kind}`).join(" · ") : "empty")}`);
|
|
777
785
|
for (const p of problems) {
|
|
@@ -1053,6 +1061,8 @@ async function checkProblems(workspace, flags = {}) {
|
|
|
1053
1061
|
}
|
|
1054
1062
|
}
|
|
1055
1063
|
|
|
1064
|
+
await checkHooks(workspace, agents, libraryDir(T), platform, note);
|
|
1065
|
+
|
|
1056
1066
|
// A script tool whose `run:` resolves to nothing parses, counts, and is
|
|
1057
1067
|
// offered to the agent — then fails inside a turn. Checking it here is the
|
|
1058
1068
|
// difference between a typo found in CI and a flow that quietly stops
|
|
@@ -1162,6 +1172,68 @@ async function checkProblems(workspace, flags = {}) {
|
|
|
1162
1172
|
return { problems, summary };
|
|
1163
1173
|
}
|
|
1164
1174
|
|
|
1175
|
+
/**
|
|
1176
|
+
* Hooks, checked with core's own reader so check and a run agree: a hook
|
|
1177
|
+
* folder that is not a hook, a `hooks:` name that resolves to nothing (a run
|
|
1178
|
+
* refuses that step rather than run it unguarded), and a tool.call hook on an
|
|
1179
|
+
* agent whose web search or fetch the model provider runs on its own servers
|
|
1180
|
+
* — those calls never pass foldrun's dispatch, so the hook never sees them.
|
|
1181
|
+
* A core from before hooks has no reader, and then there is nothing to say.
|
|
1182
|
+
*/
|
|
1183
|
+
async function checkHooks(workspace, agents, libraryRoot, platform, note) {
|
|
1184
|
+
const h = await hooksCore();
|
|
1185
|
+
if (!h) return;
|
|
1186
|
+
for (const p of h.checkHooksDir(path.join(workspace, "hooks"), "workspace")) {
|
|
1187
|
+
const m = /^(hooks\/[^/:]+(?:\/hook\.md)?): (.*)$/s.exec(p);
|
|
1188
|
+
note("error", m ? m[1] : "hooks/", m ? m[2] : p);
|
|
1189
|
+
}
|
|
1190
|
+
const matter = (await import("gray-matter")).default;
|
|
1191
|
+
const front = (file) => {
|
|
1192
|
+
try {
|
|
1193
|
+
return fs.existsSync(file) ? matter(fs.readFileSync(file, "utf8")).data ?? {} : {};
|
|
1194
|
+
} catch {
|
|
1195
|
+
return {}; // an unreadable file is reported by its own check
|
|
1196
|
+
}
|
|
1197
|
+
};
|
|
1198
|
+
const exists = (n) =>
|
|
1199
|
+
fs.existsSync(path.join(workspace, "hooks", n)) || fs.existsSync(path.join(libraryRoot, "hooks", n)) || platform.hooks?.has(n);
|
|
1200
|
+
const missing = (where, names) => {
|
|
1201
|
+
for (const n of names) {
|
|
1202
|
+
if (!exists(n)) note("error", where, `hooks: [${n}] — no hooks/${n}/hook.md in this workspace or the account library`);
|
|
1203
|
+
}
|
|
1204
|
+
};
|
|
1205
|
+
const accountNames = h.hookNames(front(path.join(path.dirname(libraryRoot), "AGENTS.md")).hooks);
|
|
1206
|
+
const workspaceNames = h.hookNames(front(path.join(workspace, "AGENTS.md")).hooks);
|
|
1207
|
+
missing("account AGENTS.md", accountNames);
|
|
1208
|
+
missing("AGENTS.md", workspaceNames);
|
|
1209
|
+
|
|
1210
|
+
let resolveSearch = null;
|
|
1211
|
+
try {
|
|
1212
|
+
({ resolveSearch } = /** @type {any} */ (await import("@foldrun/core/providers")));
|
|
1213
|
+
} catch {
|
|
1214
|
+
// no provider table: the provider-side search warning is skipped
|
|
1215
|
+
}
|
|
1216
|
+
for (const a of agents) {
|
|
1217
|
+
const f = front(path.join(workspace, "agents", a.name, "agent.md"));
|
|
1218
|
+
const own = h.hookNames(f.hooks);
|
|
1219
|
+
missing(`agents/${a.name}`, own);
|
|
1220
|
+
if (!resolveSearch || !f.web || typeof f.web !== "object" || Array.isArray(f.web)) continue;
|
|
1221
|
+
const { hooks } = h.resolveHooks({ workspaceRoot: workspace, libraryRoot, names: [...accountNames, ...workspaceNames, ...own] });
|
|
1222
|
+
const gates = hooks.filter((x) => x.on === "tool.call").map((x) => x.name);
|
|
1223
|
+
if (!gates.length) continue;
|
|
1224
|
+
for (const action of ["search", "fetch"]) {
|
|
1225
|
+
if (f.web[action] === undefined) continue;
|
|
1226
|
+
const r = resolveSearch(f.web[action], action);
|
|
1227
|
+
if (!r?.provider || r.error || r.shape === "direct") continue;
|
|
1228
|
+
note(
|
|
1229
|
+
"warn",
|
|
1230
|
+
`agents/${a.name}`,
|
|
1231
|
+
`web.${action}: ${r.provider} runs on the model provider's servers, so tool.call hook${gates.length === 1 ? "" : "s"} ${gates.join(", ")} never see${gates.length === 1 ? "s" : ""} those calls — leave web.${action}: unset (or name a ${action} API) for foldrun's own dispatch, which hooks guard`,
|
|
1232
|
+
);
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1165
1237
|
/** The web tool the platform's gallery serves: built in, and the only
|
|
1166
1238
|
* built-in that takes a provider. */
|
|
1167
1239
|
const WEB_TOOL = "web";
|
|
@@ -1907,7 +1979,47 @@ export function semverNewer(a, b) {
|
|
|
1907
1979
|
for (let i = 0; i < 3; i++) if (x.n[i] !== y.n[i]) return x.n[i] > y.n[i];
|
|
1908
1980
|
if (x.pre && !y.pre) return false;
|
|
1909
1981
|
if (!x.pre && y.pre) return true;
|
|
1910
|
-
|
|
1982
|
+
if (!x.pre) return false;
|
|
1983
|
+
// SemVer 2.0.0 §11: dot-separated identifiers compared one by one — numbers
|
|
1984
|
+
// as numbers (rc.10 > rc.9), a number before a word, and a shorter list
|
|
1985
|
+
// before a longer one it begins.
|
|
1986
|
+
const xs = x.pre.slice(1).split(".");
|
|
1987
|
+
const ys = y.pre.slice(1).split(".");
|
|
1988
|
+
for (let i = 0; i < Math.max(xs.length, ys.length); i++) {
|
|
1989
|
+
if (xs[i] == null || ys[i] == null) return ys[i] == null;
|
|
1990
|
+
const xn = /^\d+$/.test(xs[i]);
|
|
1991
|
+
const yn = /^\d+$/.test(ys[i]);
|
|
1992
|
+
if (xn && yn) {
|
|
1993
|
+
if (Number(xs[i]) !== Number(ys[i])) return Number(xs[i]) > Number(ys[i]);
|
|
1994
|
+
} else if (xn !== yn) return yn;
|
|
1995
|
+
else if (xs[i] !== ys[i]) return xs[i] > ys[i];
|
|
1996
|
+
}
|
|
1997
|
+
return false;
|
|
1998
|
+
}
|
|
1999
|
+
|
|
2000
|
+
/**
|
|
2001
|
+
* Is `latest` an update worth telling someone running `current` about? Newer,
|
|
2002
|
+
* and not a pre-release offered to someone on a stable release: npm keeps
|
|
2003
|
+
* pre-releases off `latest`, so `npm i -g foldrun@latest` would not even
|
|
2004
|
+
* install it.
|
|
2005
|
+
* @param {string} latest @param {string} current
|
|
2006
|
+
*/
|
|
2007
|
+
export function updateTo(latest, current) {
|
|
2008
|
+
// The whole header must be a version: it is printed, and a platform that
|
|
2009
|
+
// could append to it could write to the terminal.
|
|
2010
|
+
if (!/^v?\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?$/.test(String(latest ?? "").trim())) return false;
|
|
2011
|
+
const pre = (v) => /^v?\d+\.\d+\.\d+-/.test(String(v ?? "").trim());
|
|
2012
|
+
return semverNewer(latest, current) && !(pre(latest) && !pre(current));
|
|
2013
|
+
}
|
|
2014
|
+
|
|
2015
|
+
/**
|
|
2016
|
+
* Text from a platform, made safe to print: no escape sequences or other
|
|
2017
|
+
* control characters, so a hostile or broken server cannot move the cursor,
|
|
2018
|
+
* retitle the terminal, write the clipboard (OSC 52) or disguise a link.
|
|
2019
|
+
* @param {unknown} s
|
|
2020
|
+
*/
|
|
2021
|
+
export function printable(s) {
|
|
2022
|
+
return String(s ?? "").replace(/[\u0000-\u0008\u000b-\u001f\u007f-\u009f]/g, "");
|
|
1911
2023
|
}
|
|
1912
2024
|
|
|
1913
2025
|
const UPDATE_EVERY_MS = 24 * 60 * 60 * 1000;
|
|
@@ -1927,14 +2039,14 @@ function updateNotice(res) {
|
|
|
1927
2039
|
if (env("FOLDRUN_NO_UPDATE_NOTICE")) return;
|
|
1928
2040
|
if (!(process.stderr.isTTY || env("FOLDRUN_FORCE_TTY") === "1")) return;
|
|
1929
2041
|
const latest = res.headers.get("x-foldrun-cli");
|
|
1930
|
-
if (!latest || !
|
|
2042
|
+
if (!latest || !updateTo(latest, CLI_VERSION)) return;
|
|
1931
2043
|
noticeDone = true;
|
|
1932
2044
|
const file = path.join(credentialsDir(), "version-check.json");
|
|
1933
2045
|
try {
|
|
1934
2046
|
const last = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
1935
2047
|
if (Date.now() - Date.parse(last.notifiedAt) < UPDATE_EVERY_MS) return;
|
|
1936
2048
|
} catch {}
|
|
1937
|
-
const platform = res.headers.get("x-foldrun-version");
|
|
2049
|
+
const platform = printable(res.headers.get("x-foldrun-version") ?? "") || null;
|
|
1938
2050
|
console.error(` ${c.dim(`foldrun ${latest} is out (this is ${CLI_VERSION})${platform ? `, released with platform ${platform}` : ""} — npm i -g foldrun@latest`)}`);
|
|
1939
2051
|
fs.mkdirSync(credentialsDir(), { recursive: true });
|
|
1940
2052
|
fs.writeFileSync(file, JSON.stringify({ notifiedAt: new Date().toISOString(), cli: latest, platform }) + "\n");
|
|
@@ -1968,7 +2080,7 @@ async function versionCmd(flags) {
|
|
|
1968
2080
|
}
|
|
1969
2081
|
}
|
|
1970
2082
|
const latestCli = platform?.packages?.cli ?? null;
|
|
1971
|
-
const behind = latestCli ?
|
|
2083
|
+
const behind = latestCli ? updateTo(latestCli, CLI_VERSION) : false;
|
|
1972
2084
|
if (flags.json === true) {
|
|
1973
2085
|
console.log(JSON.stringify({ cli: CLI_VERSION, core, platform: platform ?? (url ? { url, error: unreachable } : null) }, null, 2));
|
|
1974
2086
|
if (behind) console.error(`foldrun ${latestCli} is out (this is ${CLI_VERSION}) — npm i -g foldrun@latest`);
|
|
@@ -3625,7 +3737,7 @@ async function auditCmd(positional, flags) {
|
|
|
3625
3737
|
q.set("format", "csv");
|
|
3626
3738
|
const { bytes, name } = await remoteBytes(url, flags, `/api/audit?${q}`, "the audit log");
|
|
3627
3739
|
if (typeof flags.file !== "string") {
|
|
3628
|
-
|
|
3740
|
+
writeExact(bytes);
|
|
3629
3741
|
return 0;
|
|
3630
3742
|
}
|
|
3631
3743
|
return writeDownload(bytes, name ?? "audit.csv", flags, "the audit log");
|
|
@@ -3701,7 +3813,7 @@ async function sourceCmd(positional, flags) {
|
|
|
3701
3813
|
if (verb === "cat" || verb === "read" || verb === "get") {
|
|
3702
3814
|
if (!a) throw new Error("which file? foldrun source cat flows/daily.md --to <workspace>");
|
|
3703
3815
|
const { content } = await remoteCall(url, flags, `${base}?path=${encodeURIComponent(a)}`);
|
|
3704
|
-
|
|
3816
|
+
writeExact(content.endsWith("\n") ? content : `${content}\n`);
|
|
3705
3817
|
return 0;
|
|
3706
3818
|
}
|
|
3707
3819
|
if (verb === "put" || verb === "write") {
|
|
@@ -4706,7 +4818,7 @@ async function storageCmd(positional, flags, layout) {
|
|
|
4706
4818
|
if (bytes.includes(0)) {
|
|
4707
4819
|
throw new Error(`${rel} is not text (${humanBytes(bytes.length)}) — \`foldrun storage cat ${rel} --preview\` reads what is in it, \`foldrun storage get ${rel}\` writes it to a file`);
|
|
4708
4820
|
}
|
|
4709
|
-
|
|
4821
|
+
writeExact(bytes);
|
|
4710
4822
|
return 0;
|
|
4711
4823
|
}
|
|
4712
4824
|
|
|
@@ -6122,6 +6234,308 @@ async function checkRemote(flags, layout) {
|
|
|
6122
6234
|
}
|
|
6123
6235
|
}
|
|
6124
6236
|
|
|
6237
|
+
// ---------------------------------------------------------------- hooks
|
|
6238
|
+
|
|
6239
|
+
/**
|
|
6240
|
+
* Core's hooks module — readHookDef, runHook, the event list — or null on a
|
|
6241
|
+
* core from before hooks, where `check` then says nothing about them and the
|
|
6242
|
+
* `hook` verbs say which core they need.
|
|
6243
|
+
*/
|
|
6244
|
+
async function hooksCore() {
|
|
6245
|
+
try {
|
|
6246
|
+
return /** @type {any} */ (await import("@foldrun/core/hooks"));
|
|
6247
|
+
} catch {
|
|
6248
|
+
return null;
|
|
6249
|
+
}
|
|
6250
|
+
}
|
|
6251
|
+
async function needHooksCore() {
|
|
6252
|
+
const h = await hooksCore();
|
|
6253
|
+
if (!h) throw new Error("this CLI's @foldrun/core has no hooks — update it (npm i -g foldrun@latest)");
|
|
6254
|
+
return h;
|
|
6255
|
+
}
|
|
6256
|
+
|
|
6257
|
+
/** The account library a hook verb works on: the folder beside workspaces/. */
|
|
6258
|
+
const libraryRootOf = (layout) => path.join(layout.accountRoot, "library");
|
|
6259
|
+
|
|
6260
|
+
/** What a new run.mjs answers, per event: a gate allows, a watcher notes nothing. */
|
|
6261
|
+
const HOOK_ANSWER = {
|
|
6262
|
+
"tool.call": `{ decision: "allow" }`,
|
|
6263
|
+
"tool.result": `{}`,
|
|
6264
|
+
"step.start": `{}`,
|
|
6265
|
+
"step.end": `{}`,
|
|
6266
|
+
};
|
|
6267
|
+
|
|
6268
|
+
/** A sample event per kind, for `hook test` with no --event. */
|
|
6269
|
+
const HOOK_SAMPLE = {
|
|
6270
|
+
"tool.call": { tool: { name: "Bash", input: { command: "ls" } } },
|
|
6271
|
+
"tool.result": { tool: { name: "Bash", input: { command: "ls" }, result: "agent.md\n" } },
|
|
6272
|
+
"step.start": { prompt: "Find three sources." },
|
|
6273
|
+
"step.end": { reply: "Done." },
|
|
6274
|
+
};
|
|
6275
|
+
|
|
6276
|
+
function hookRunTemplate(name, on) {
|
|
6277
|
+
return `// ${name} — a foldrun hook (on: ${on}). Self-contained: Node's built-ins only,
|
|
6278
|
+
// no relative imports — it runs from a copy taken when the step starts.
|
|
6279
|
+
//
|
|
6280
|
+
// In: one JSON object on stdin — the event:
|
|
6281
|
+
// { event, hook, run_id, step, agent, workspace,
|
|
6282
|
+
// tool: { name, input } (tool.call, tool.result; + result on tool.result)
|
|
6283
|
+
// prompt (step.start)
|
|
6284
|
+
// reply (step.end) }
|
|
6285
|
+
// Out: one JSON object on stdout, the last line printed:
|
|
6286
|
+
// tool.call { decision: "allow" | "deny", reason, input, note }
|
|
6287
|
+
// input replaces the tool's whole input; a deny needs a reason
|
|
6288
|
+
// step.start { context, note } context is added to the step's prompt
|
|
6289
|
+
// step.end { reply, note } reply replaces the step's reply
|
|
6290
|
+
// tool.result { note } a line on the run's trail
|
|
6291
|
+
// Exit 0 with an answer. Exit 2 denies, with stderr as the reason.
|
|
6292
|
+
// Anything else is a failure: a gate (tool.call, step.start) that fails
|
|
6293
|
+
// refuses; a watcher (tool.result, step.end) that fails is recorded.
|
|
6294
|
+
// No secrets and no model key reach this process.
|
|
6295
|
+
|
|
6296
|
+
let raw = "";
|
|
6297
|
+
for await (const chunk of process.stdin) raw += chunk;
|
|
6298
|
+
const event = JSON.parse(raw || "{}");
|
|
6299
|
+
|
|
6300
|
+
// Decide here. This one allows everything.
|
|
6301
|
+
void event;
|
|
6302
|
+
console.log(JSON.stringify(${HOOK_ANSWER[on]}));
|
|
6303
|
+
`;
|
|
6304
|
+
}
|
|
6305
|
+
|
|
6306
|
+
/** `foldrun hook <ls|add|test|rm>` — hooks in this workspace and the library. */
|
|
6307
|
+
async function hookCmd(positional, flags, layout) {
|
|
6308
|
+
const [verb = "ls"] = positional;
|
|
6309
|
+
if (verb === "ls" || verb === "list") return hookLsCmd(flags, layout);
|
|
6310
|
+
if (verb === "add" || verb === "new") return hookAddCmd(positional, flags, layout);
|
|
6311
|
+
if (verb === "test") return hookTestCmd(positional, flags, layout);
|
|
6312
|
+
if (verb === "rm") return hookRmCmd(positional, flags, layout);
|
|
6313
|
+
throw new Error(`unknown hook verb "${verb}" — ls, add <name> --on <event>, test <name>, rm <name>`);
|
|
6314
|
+
}
|
|
6315
|
+
|
|
6316
|
+
/** Every hook folder here, the workspace's first, each read the way a run reads it. */
|
|
6317
|
+
async function hooksHere(flags, layout) {
|
|
6318
|
+
const h = await needHooksCore();
|
|
6319
|
+
const ws = workspaceDirFor(flags, layout);
|
|
6320
|
+
const out = [];
|
|
6321
|
+
const seen = new Set();
|
|
6322
|
+
for (const [root, scope] of [[ws, "workspace"], [libraryRootOf(layout), "account"]]) {
|
|
6323
|
+
for (const dir of h.listHookDirs(path.join(root, "hooks"))) {
|
|
6324
|
+
const name = path.basename(dir);
|
|
6325
|
+
const def = h.readHookDef(dir, scope);
|
|
6326
|
+
const row = "error" in def
|
|
6327
|
+
? { name, scope, error: def.error }
|
|
6328
|
+
: { name, scope, on: def.on, tool: def.tool ?? null, timeout: def.timeoutSec, description: def.description ?? null };
|
|
6329
|
+
// A workspace hook of the same name is the one a run resolves.
|
|
6330
|
+
out.push({ ...row, ...(seen.has(name) ? { shadowed: true } : {}) });
|
|
6331
|
+
seen.add(name);
|
|
6332
|
+
}
|
|
6333
|
+
}
|
|
6334
|
+
return { h, ws, hooks: out };
|
|
6335
|
+
}
|
|
6336
|
+
|
|
6337
|
+
/** `hook ls --to <ws>`: the DEPLOYED workspace's hooks and what they did
|
|
6338
|
+
* lately — GET /api/workspaces/<ws>/hooks, the dashboard's Hooks panel. */
|
|
6339
|
+
async function hookLsRemote(flags) {
|
|
6340
|
+
const url = platformFor(flags, "hook ls");
|
|
6341
|
+
const ws = String(flags.to);
|
|
6342
|
+
const q = new URLSearchParams();
|
|
6343
|
+
if (flags.all === true) q.set("all", "1");
|
|
6344
|
+
if (flags.runs !== undefined) q.set("runs", String(flags.runs));
|
|
6345
|
+
const r = /** @type {{ hooks: any[], recent: any[] }} */ (await remoteCall(url, flags, `/api/workspaces/${encodeURIComponent(ws)}/hooks${q.size ? `?${q}` : ""}`));
|
|
6346
|
+
if (flags.json === true) {
|
|
6347
|
+
console.log(JSON.stringify(r, null, 2));
|
|
6348
|
+
return 0;
|
|
6349
|
+
}
|
|
6350
|
+
const hooks = r.hooks ?? [];
|
|
6351
|
+
if (!hooks.length) console.log(`\n ${c.dim(`no hooks in ${ws} or the account library`)}`);
|
|
6352
|
+
else {
|
|
6353
|
+
console.log();
|
|
6354
|
+
const w = Math.max(...hooks.map((x) => x.name.length));
|
|
6355
|
+
for (const x of hooks) {
|
|
6356
|
+
const where = x.scope === "account" ? "library" : "workspace";
|
|
6357
|
+
if (x.error) {
|
|
6358
|
+
console.log(` ${c.red("✗")} ${c.bold(x.name.padEnd(w))} ${c.dim(where.padEnd(9))} ${x.error}`);
|
|
6359
|
+
continue;
|
|
6360
|
+
}
|
|
6361
|
+
const on = (x.onFor ?? []).length ? `on for ${x.onFor.join(", ")}` : "turned on by nobody";
|
|
6362
|
+
const note = x.shadowed ? "(the workspace's own of this name is the one that runs)" : `${on}${x.description ? ` — ${x.description}` : ""}`;
|
|
6363
|
+
console.log(` ${c.green("✓")} ${c.bold(x.name.padEnd(w))} ${c.dim(where.padEnd(9))} ${String(x.on).padEnd(11)} ${String(x.tool ?? "*").padEnd(12)} ${c.dim(note)}`);
|
|
6364
|
+
}
|
|
6365
|
+
}
|
|
6366
|
+
const recent = r.recent ?? [];
|
|
6367
|
+
console.log(`\n ${c.bold("lately")} ${c.dim(recent.length ? `${recent.length} ${flags.all === true ? "hook decision" : "deny, rewrite or failure"}${recent.length === 1 ? "" : "s"}, newest first` : flags.all === true ? "no hook ran" : "nothing denied, rewritten or failed (--all for every decision)")}`);
|
|
6368
|
+
for (const e of recent.slice(0, 20)) {
|
|
6369
|
+
const rec = e.record ?? {};
|
|
6370
|
+
console.log(` ${c.dim(String(e.t).slice(0, 16).replace("T", " "))} ${rec.hook} ${rec.did} ${rec.event}${rec.tool ? ` ${rec.tool}` : ""} ${c.dim(`${e.runId} step ${e.step} ${e.agent}`)}${rec.reason ? ` — ${firstLine(rec.reason, 80)}` : ""}`);
|
|
6371
|
+
}
|
|
6372
|
+
console.log(`\n ${c.dim("--runs <n> reads further back (default 50) · --all for every decision · --json")}\n`);
|
|
6373
|
+
return hooks.some((x) => x.error) ? 1 : 0;
|
|
6374
|
+
}
|
|
6375
|
+
|
|
6376
|
+
async function hookLsCmd(flags, layout) {
|
|
6377
|
+
if (typeof flags.to === "string") return hookLsRemote(flags);
|
|
6378
|
+
const { hooks } = await hooksHere(flags, layout);
|
|
6379
|
+
if (flags.json === true) {
|
|
6380
|
+
console.log(JSON.stringify(hooks, null, 2));
|
|
6381
|
+
return 0;
|
|
6382
|
+
}
|
|
6383
|
+
if (!hooks.length) {
|
|
6384
|
+
console.log(`\n ${c.dim("no hooks — `foldrun hook add <name> --on tool.call` makes one")}\n`);
|
|
6385
|
+
return 0;
|
|
6386
|
+
}
|
|
6387
|
+
console.log();
|
|
6388
|
+
const w = Math.max(...hooks.map((x) => x.name.length));
|
|
6389
|
+
for (const x of hooks) {
|
|
6390
|
+
const where = x.scope === "account" ? "library" : "workspace";
|
|
6391
|
+
if (x.error) {
|
|
6392
|
+
console.log(` ${c.red("✗")} ${c.bold(x.name.padEnd(w))} ${c.dim(where.padEnd(9))} ${x.error}`);
|
|
6393
|
+
continue;
|
|
6394
|
+
}
|
|
6395
|
+
const what = `${x.on.padEnd(11)} ${(x.tool ?? "*").padEnd(12)}`;
|
|
6396
|
+
const note = x.shadowed ? c.dim("(the workspace's own of this name is the one that runs)") : c.dim(x.description ?? "");
|
|
6397
|
+
console.log(` ${c.green("✓")} ${c.bold(x.name.padEnd(w))} ${c.dim(where.padEnd(9))} ${what} ${note}`);
|
|
6398
|
+
}
|
|
6399
|
+
console.log(`\n ${c.dim("an agent turns one on with hooks: [name]; AGENTS.md hooks: turns it on for every agent under it")}\n`);
|
|
6400
|
+
return hooks.some((x) => x.error) ? 1 : 0;
|
|
6401
|
+
}
|
|
6402
|
+
|
|
6403
|
+
async function hookAddCmd(positional, flags, layout) {
|
|
6404
|
+
const h = await needHooksCore();
|
|
6405
|
+
const name = positional[1];
|
|
6406
|
+
if (!name) throw new Error("which hook? `foldrun hook add <name> --on tool.call|tool.result|step.start|step.end`");
|
|
6407
|
+
if (!/^[a-z0-9][a-z0-9-]{0,63}$/.test(name)) {
|
|
6408
|
+
throw new Error(`"${name}" is not a hook name — lowercase letters, digits and dashes, e.g. no-rm-rf`);
|
|
6409
|
+
}
|
|
6410
|
+
const on = flags.on;
|
|
6411
|
+
if (typeof on !== "string" || !h.HOOK_EVENTS.includes(on)) {
|
|
6412
|
+
throw new Error(`--on takes ${h.HOOK_EVENTS.join(", ")}${typeof on === "string" ? ` — not "${on}"` : ""}`);
|
|
6413
|
+
}
|
|
6414
|
+
const tool = typeof flags.tool === "string" ? flags.tool.trim() : undefined;
|
|
6415
|
+
if (flags.tool !== undefined && !tool) throw new Error("--tool takes a tool name or pattern, like Bash, Write|Edit or mcp__*");
|
|
6416
|
+
if (tool && on !== "tool.call" && on !== "tool.result") {
|
|
6417
|
+
throw new Error(`--tool only means something on tool.call and tool.result, not ${on}`);
|
|
6418
|
+
}
|
|
6419
|
+
const root = flags.library === true ? libraryRootOf(layout) : workspaceDirFor(flags, layout);
|
|
6420
|
+
const rel = `hooks/${name}`;
|
|
6421
|
+
const front = [
|
|
6422
|
+
"---",
|
|
6423
|
+
`on: ${on}`,
|
|
6424
|
+
...(tool ? [`tool: ${/[|*:]/.test(tool) ? JSON.stringify(tool) : tool}`] : []),
|
|
6425
|
+
`description: What ${name} checks, in one line.`,
|
|
6426
|
+
"---",
|
|
6427
|
+
"",
|
|
6428
|
+
`What ${name} is for, and what it refuses or rewrites. The code is run.mjs beside this file.`,
|
|
6429
|
+
"",
|
|
6430
|
+
].join("\n");
|
|
6431
|
+
writeFiles(root, [
|
|
6432
|
+
{ path: `${rel}/hook.md`, content: front },
|
|
6433
|
+
{ path: `${rel}/run.mjs`, content: hookRunTemplate(name, on) },
|
|
6434
|
+
], flags.library === true ? "the library" : path.basename(root));
|
|
6435
|
+
const where = path.relative(process.cwd(), path.join(root, rel)) || rel;
|
|
6436
|
+
console.log(`\n ${c.green("created")} hook ${c.bold(name)} ${c.dim(`(${on}${tool ? ` · ${tool}` : ""})`)} in ${where}\n`);
|
|
6437
|
+
console.log(` ${c.dim(`${rel}/hook.md`)}\n ${c.dim(`${rel}/run.mjs`)}`);
|
|
6438
|
+
console.log(`\n ${c.bold("Next")}
|
|
6439
|
+
edit ${path.join(where, "run.mjs")}
|
|
6440
|
+
hooks: [${name}]${" ".repeat(4)}${c.dim("in an agent's frontmatter — or in AGENTS.md for every agent")}
|
|
6441
|
+
foldrun hook test ${name}${" ".repeat(4)}${c.dim("run it on a sample event, here")}\n`);
|
|
6442
|
+
return 0;
|
|
6443
|
+
}
|
|
6444
|
+
|
|
6445
|
+
/** --event: inline JSON, or @file. */
|
|
6446
|
+
function readHookEvent(raw, on) {
|
|
6447
|
+
if (raw === undefined) return { event: HOOK_SAMPLE[on] ?? {}, sample: true };
|
|
6448
|
+
if (typeof raw !== "string") throw new Error("--event takes a JSON object, or @file");
|
|
6449
|
+
const text = raw.startsWith("@") ? fs.readFileSync(raw.slice(1), "utf8") : raw;
|
|
6450
|
+
let event;
|
|
6451
|
+
try { event = JSON.parse(text); } catch (err) {
|
|
6452
|
+
throw new Error(`--event is not JSON (${err instanceof Error ? err.message : err})`);
|
|
6453
|
+
}
|
|
6454
|
+
if (!event || typeof event !== "object" || Array.isArray(event)) throw new Error("--event is one JSON object — the event the hook reads on stdin");
|
|
6455
|
+
return { event, sample: false };
|
|
6456
|
+
}
|
|
6457
|
+
|
|
6458
|
+
function printHookAnswer(name, on, a, where) {
|
|
6459
|
+
const mark = !a.ok ? c.red("✗") : a.decision === "deny" ? c.amber("■") : c.green("✓");
|
|
6460
|
+
console.log(`\n ${mark} ${c.bold(name)} ${c.dim(`${on ?? "?"} · ${where} · ${a.ms ?? 0}ms`)}`);
|
|
6461
|
+
if (!a.ok) console.log(` ${c.red("failed")} ${a.failure ?? "no reason given"}${on === "tool.call" || on === "step.start" ? c.dim(" — a gate that fails refuses") : ""}`);
|
|
6462
|
+
if (a.decision) console.log(` ${pad("decision", 9)} ${a.decision === "deny" ? c.amber("deny") : "allow"}`);
|
|
6463
|
+
if (a.reason) console.log(` ${pad("reason", 9)} ${a.reason}`);
|
|
6464
|
+
if (a.input) console.log(` ${pad("input", 9)} ${JSON.stringify(a.input)}`);
|
|
6465
|
+
if (a.context) console.log(` ${pad("context", 9)} ${a.context}`);
|
|
6466
|
+
if (a.reply !== undefined) console.log(` ${pad("reply", 9)} ${a.reply}`);
|
|
6467
|
+
if (a.note) console.log(` ${pad("note", 9)} ${a.note}`);
|
|
6468
|
+
console.log();
|
|
6469
|
+
return a.ok ? 0 : 1;
|
|
6470
|
+
}
|
|
6471
|
+
|
|
6472
|
+
/**
|
|
6473
|
+
* `foldrun hook test <name> [--event '<json>']` — run one hook on one event,
|
|
6474
|
+
* here, the way a step runs it (core's runHook: the event on stdin, a
|
|
6475
|
+
* stripped environment, its timeout). `--to <workspace>` asks the platform
|
|
6476
|
+
* to run the deployed one instead.
|
|
6477
|
+
*/
|
|
6478
|
+
async function hookTestCmd(positional, flags, layout) {
|
|
6479
|
+
const name = positional[1];
|
|
6480
|
+
if (!name) throw new Error("which hook? `foldrun hook test <name> --event '<json>'` — `foldrun hook ls` lists them");
|
|
6481
|
+
if (typeof flags.to === "string") {
|
|
6482
|
+
const url = platformFor(flags, "hook test");
|
|
6483
|
+
const ws = flags.to;
|
|
6484
|
+
let event = {};
|
|
6485
|
+
if (flags.event !== undefined) event = readHookEvent(flags.event, "tool.call").event;
|
|
6486
|
+
const r = /** @type {any} */ (await remoteCall(
|
|
6487
|
+
url,
|
|
6488
|
+
flags,
|
|
6489
|
+
`/api/workspaces/${enc(ws)}/hooks/${enc(name)}/test`,
|
|
6490
|
+
{ method: "POST", body: JSON.stringify({ event }) },
|
|
6491
|
+
));
|
|
6492
|
+
const a = r?.answer ?? r ?? {};
|
|
6493
|
+
return printHookAnswer(name, r?.on ?? a.on, { ok: a.ok !== false, ...a }, ws);
|
|
6494
|
+
}
|
|
6495
|
+
const h = await needHooksCore();
|
|
6496
|
+
const ws = workspaceDirFor({ ...flags, to: undefined }, layout);
|
|
6497
|
+
const r = h.resolveHooks({ workspaceRoot: ws, libraryRoot: libraryRootOf(layout), names: [name] });
|
|
6498
|
+
if (r.missing.length) throw new Error(`no hook named ${name} in hooks/ or the library — \`foldrun hook ls\` lists them`);
|
|
6499
|
+
if (r.errors.length) throw new Error(r.errors[0]);
|
|
6500
|
+
const def = r.hooks[0];
|
|
6501
|
+
const { event, sample } = readHookEvent(flags.event, def.on);
|
|
6502
|
+
if (sample) console.log(`\n ${c.dim(`no --event, so a sample ${def.on} event: ${JSON.stringify(event)}`)}`);
|
|
6503
|
+
const full = { event: def.on, hook: def.name, run_id: null, step: null, agent: null, workspace: path.basename(ws), ...event };
|
|
6504
|
+
const a = await h.runHook(def, full, { cwd: ws, env: process.env });
|
|
6505
|
+
return printHookAnswer(name, def.on, a, def.scope === "account" ? "library" : path.basename(ws));
|
|
6506
|
+
}
|
|
6507
|
+
|
|
6508
|
+
async function hookRmCmd(positional, flags, layout) {
|
|
6509
|
+
const h = await needHooksCore();
|
|
6510
|
+
const name = positional[1];
|
|
6511
|
+
if (!name) throw new Error("which hook? `foldrun hook rm <name>` (--library for the account's)");
|
|
6512
|
+
const root = flags.library === true ? libraryRootOf(layout) : workspaceDirFor(flags, layout);
|
|
6513
|
+
const dir = path.join(root, "hooks", name);
|
|
6514
|
+
if (!/^[a-z0-9][a-z0-9-]{0,63}$/.test(name) || !fs.existsSync(dir)) {
|
|
6515
|
+
throw new Error(`no hooks/${name} in ${flags.library === true ? "the library" : path.basename(root)} — \`foldrun hook ls\` lists them`);
|
|
6516
|
+
}
|
|
6517
|
+
// Who still turns it on: they fail check, and their steps are refused.
|
|
6518
|
+
const users = [];
|
|
6519
|
+
const wsRoots = flags.library === true
|
|
6520
|
+
? (layout.workspacesDir ? layout.workspaces.map((w) => path.join(layout.workspacesDir, w)) : [layout.workspaceDir].filter(Boolean))
|
|
6521
|
+
: [root];
|
|
6522
|
+
const matter = (await import("gray-matter")).default;
|
|
6523
|
+
const names = (file) => {
|
|
6524
|
+
try { return h.hookNames(matter(fs.readFileSync(file, "utf8")).data?.hooks); } catch { return []; }
|
|
6525
|
+
};
|
|
6526
|
+
for (const w of wsRoots) {
|
|
6527
|
+
if (names(path.join(w, "AGENTS.md")).includes(name)) users.push(`${path.basename(w)}/AGENTS.md`);
|
|
6528
|
+
for (const a of ls(path.join(w, "agents"))) {
|
|
6529
|
+
if (names(path.join(w, "agents", a, "agent.md")).includes(name)) users.push(`${path.basename(w)}/agents/${a}`);
|
|
6530
|
+
}
|
|
6531
|
+
}
|
|
6532
|
+
const still = users.length ? ` — still turned on by ${users.join(", ")}, whose steps will be refused until it goes from their hooks:` : "";
|
|
6533
|
+
if (!(await sureToDelete(flags, "hook rm", `${flags.library === true ? "library/" : ""}hooks/${name}${still}`))) return 1;
|
|
6534
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
6535
|
+
console.log(`\n ${c.green("✓")} removed hooks/${name}${users.length ? c.dim(` — take it out of: ${users.join(", ")}`) : ""}\n`);
|
|
6536
|
+
return 0;
|
|
6537
|
+
}
|
|
6538
|
+
|
|
6125
6539
|
// ------------------------------------------- flows and teams, as the canvas edits them
|
|
6126
6540
|
//
|
|
6127
6541
|
// The dashboard's flow canvas turns every palette block into a small, exact
|
|
@@ -7138,7 +7552,7 @@ async function runRmCmd(runId, flags, layout) {
|
|
|
7138
7552
|
/** Where downloaded bytes go: --file (or - for stdout), else a default name. Refuses to clobber. */
|
|
7139
7553
|
function writeDownload(bytes, fallback, flags, from) {
|
|
7140
7554
|
if (flags.file === "-") {
|
|
7141
|
-
|
|
7555
|
+
writeExact(bytes);
|
|
7142
7556
|
return 0;
|
|
7143
7557
|
}
|
|
7144
7558
|
const to = typeof flags.file === "string" ? flags.file : fallback;
|
|
@@ -7174,7 +7588,7 @@ async function apiCmd(positional, flags) {
|
|
|
7174
7588
|
const out = typeof flags.out === "string" ? flags.out : typeof flags.file === "string" ? flags.file : "-";
|
|
7175
7589
|
const pretty = JSON.stringify(doc, null, 2) + "\n";
|
|
7176
7590
|
if (out === "-") {
|
|
7177
|
-
|
|
7591
|
+
writeExact(pretty);
|
|
7178
7592
|
return 0;
|
|
7179
7593
|
}
|
|
7180
7594
|
fs.writeFileSync(out, pretty);
|
|
@@ -7461,7 +7875,7 @@ async function billingVerbCmd(positional, flags) {
|
|
|
7461
7875
|
if (flags.csv === true) {
|
|
7462
7876
|
const { bytes, name } = await remoteBytes(url, flags, `/api/billing/statement?month=${month}&format=csv`, `statement ${month}`);
|
|
7463
7877
|
if (typeof flags.file !== "string") {
|
|
7464
|
-
|
|
7878
|
+
writeExact(bytes);
|
|
7465
7879
|
return 0;
|
|
7466
7880
|
}
|
|
7467
7881
|
return writeDownload(bytes, name ?? `foldrun-${month}.csv`, flags, `the ${month} statement`);
|
|
@@ -8495,7 +8909,7 @@ async function scheduleTickCmd(flags) {
|
|
|
8495
8909
|
return 0;
|
|
8496
8910
|
}
|
|
8497
8911
|
|
|
8498
|
-
const LIBRARY_KINDS_CLI = ["skills", "tools", "scripts", "knowledge", "memory"];
|
|
8912
|
+
const LIBRARY_KINDS_CLI = ["skills", "tools", "hooks", "scripts", "knowledge", "memory"];
|
|
8499
8913
|
|
|
8500
8914
|
/**
|
|
8501
8915
|
* `foldrun library [ls [kind]] | cat <kind>/<path> | put <kind>/<path> | rm
|
|
@@ -8515,9 +8929,16 @@ async function libraryCmd(positional, flags) {
|
|
|
8515
8929
|
if (verb === "ls" || verb === "list") {
|
|
8516
8930
|
const kinds = spec ? [spec] : LIBRARY_KINDS_CLI;
|
|
8517
8931
|
for (const k of kinds) if (!LIBRARY_KINDS_CLI.includes(k)) throw new Error(`no library kind "${k}" — ${LIBRARY_KINDS_CLI.join(", ")}`);
|
|
8518
|
-
|
|
8932
|
+
// Listing everything, a kind an older platform does not have yet (hooks
|
|
8933
|
+
// before 11 Oct 2026) is left out rather than failing the whole list;
|
|
8934
|
+
// asked for by name, it fails with the platform's own answer.
|
|
8935
|
+
const lists = await Promise.all(kinds.map((k) => remoteCall(url, flags, `/api/library/${k}`).catch((err) => {
|
|
8936
|
+
if (spec) throw err;
|
|
8937
|
+
return null;
|
|
8938
|
+
})));
|
|
8519
8939
|
console.log();
|
|
8520
8940
|
lists.forEach((l, n) => {
|
|
8941
|
+
if (!l) return;
|
|
8521
8942
|
const entries = l.entries ?? [];
|
|
8522
8943
|
console.log(` ${c.bold(kinds[n])} ${c.dim(`${entries.length}`)}`);
|
|
8523
8944
|
for (const e of entries) console.log(` ${pad(e.name ?? e.path ?? "", 28)} ${c.dim(firstLine(e.description ?? "", 70))}`);
|
|
@@ -8528,7 +8949,7 @@ async function libraryCmd(positional, flags) {
|
|
|
8528
8949
|
if (verb === "cat") {
|
|
8529
8950
|
const { kind, rel } = split(spec);
|
|
8530
8951
|
const { content } = await remoteCall(url, flags, `/api/library/${kind}?path=${enc(rel)}`);
|
|
8531
|
-
|
|
8952
|
+
writeExact(content ?? "");
|
|
8532
8953
|
return 0;
|
|
8533
8954
|
}
|
|
8534
8955
|
if (verb === "put") {
|
|
@@ -8797,10 +9218,29 @@ async function apiVersionCmd(positional, flags) {
|
|
|
8797
9218
|
console.log(`\n ${c.bold("API version")} ${v.effective} ${c.dim(v.pinned ? `pinned (current is ${v.current})` : "current — not pinned")}`);
|
|
8798
9219
|
console.log(` ${c.dim(`served: ${(v.supported ?? []).join(", ") || "—"} · each for ${v.supportMonths ?? "?"} months after the next · this CLI sends ${API_VERSION}`)}`);
|
|
8799
9220
|
if (l.perCredential) console.log(` ${c.bold("rate limits")} ${c.dim(`${l.plan} · per key ${l.perCredential.read} reads / ${l.perCredential.write} writes, per account ${l.perAccount?.read} / ${l.perAccount?.write} — every ${l.windowSeconds}s`)}`);
|
|
9221
|
+
apiUsageLines(facts.usage);
|
|
8800
9222
|
console.log(`\n ${c.dim("foldrun api version pin YYYY-MM-DD · unpin")}\n`);
|
|
8801
9223
|
return 0;
|
|
8802
9224
|
}
|
|
8803
9225
|
|
|
9226
|
+
/** The account's use of the API, last 30 days: requests by version, and any
|
|
9227
|
+
* deprecated route it still calls, by whom, and when it stops. An older
|
|
9228
|
+
* platform sends no `usage`; null means it could not be read. */
|
|
9229
|
+
function apiUsageLines(usage) {
|
|
9230
|
+
if (usage === undefined) return;
|
|
9231
|
+
if (usage === null) {
|
|
9232
|
+
console.log(` ${c.bold("your use")} ${c.dim("could not be read just now")}`);
|
|
9233
|
+
return;
|
|
9234
|
+
}
|
|
9235
|
+
const total = (usage.versions ?? []).reduce((s, v) => s + (v.requests ?? 0), 0);
|
|
9236
|
+
const day = (iso) => String(iso ?? "").slice(0, 10);
|
|
9237
|
+
console.log(` ${c.bold("your use")} ${c.dim(total ? `last ${usage.days} days: ` + usage.versions.map((v) => `${v.version} ${v.requests} (last ${day(v.lastSeen)})`).join(" · ") : `no API requests in the last ${usage.days} days`)}`);
|
|
9238
|
+
for (const d of usage.deprecated ?? []) {
|
|
9239
|
+
console.log(` ${c.amber("deprecated")} ${d.route} — ${d.requests} request${d.requests === 1 ? "" : "s"}, last ${day(d.lastSeen)}; ${d.sunsetAt ? `stops answering ${day(d.sunsetAt)}` : "no end date yet"}${d.link ? ` · ${d.link}` : ""}`);
|
|
9240
|
+
for (const who of d.callers ?? []) console.log(` ${c.dim(`${who.credential || "an unknown caller"}: ${who.requests}, last ${day(who.lastSeen)}`)}`);
|
|
9241
|
+
}
|
|
9242
|
+
}
|
|
9243
|
+
|
|
8804
9244
|
const THEMES_CLI = ["system", "light", "dark"];
|
|
8805
9245
|
|
|
8806
9246
|
/**
|
|
@@ -8828,7 +9268,8 @@ async function preferencesCmd(positional, flags) {
|
|
|
8828
9268
|
return 0;
|
|
8829
9269
|
}
|
|
8830
9270
|
|
|
8831
|
-
/** `foldrun changelog [--limit n]` — What's new:
|
|
9271
|
+
/** `foldrun changelog [--limit n]` — What's new: the API's versions, what each
|
|
9272
|
+
* broke and what is deprecated, then each release and what it changed. */
|
|
8832
9273
|
async function changelogCmd(flags) {
|
|
8833
9274
|
const url = platformFor(flags, "changelog");
|
|
8834
9275
|
const limit = Number(flags.limit) > 0 ? Math.min(50, Math.floor(Number(flags.limit))) : 5;
|
|
@@ -8837,6 +9278,7 @@ async function changelogCmd(flags) {
|
|
|
8837
9278
|
console.log(JSON.stringify(r, null, 2));
|
|
8838
9279
|
return 0;
|
|
8839
9280
|
}
|
|
9281
|
+
if (r.api) apiChangelogLines(r.api);
|
|
8840
9282
|
const releases = r.releases ?? [];
|
|
8841
9283
|
if (!releases.length) {
|
|
8842
9284
|
console.log(`\n ${c.dim(`no release notes on this install (it runs ${r.version ?? "an unreleased build"})`)}\n`);
|
|
@@ -8859,6 +9301,25 @@ async function changelogCmd(flags) {
|
|
|
8859
9301
|
return 0;
|
|
8860
9302
|
}
|
|
8861
9303
|
|
|
9304
|
+
/** The API part of What's new: the current version, the breaking changes by
|
|
9305
|
+
* version, and routes on their way out. Older platforms send no `api`. */
|
|
9306
|
+
function apiChangelogLines(api) {
|
|
9307
|
+
const p = printable;
|
|
9308
|
+
const broke = (api.versions ?? []).filter((v) => v.changes?.length);
|
|
9309
|
+
const deps = api.deprecations ?? [];
|
|
9310
|
+
console.log(`\n ${c.bold(`API ${p(api.current)}`)} ${c.dim(`each version works ${Number(api.supportMonths) || 12} months or more after the next · pin: foldrun api version pin <date>`)}`);
|
|
9311
|
+
if (!broke.length && !deps.length) {
|
|
9312
|
+
console.log(` ${c.dim("nothing existing code would break on, and nothing deprecated")}`);
|
|
9313
|
+
return;
|
|
9314
|
+
}
|
|
9315
|
+
if (broke.length) console.log(` ${c.dim("breaking changes")}`);
|
|
9316
|
+
for (const v of broke) for (const ch of v.changes) console.log(` · ${p(v.version)} ${firstLine(p(ch), 100)}`);
|
|
9317
|
+
if (deps.length) console.log(` ${c.dim("deprecated")}`);
|
|
9318
|
+
for (const d of deps) {
|
|
9319
|
+
console.log(` · ${p(d.route)} — since ${p(d.deprecatedAt)}, ${d.sunsetAt ? `stops answering ${p(d.sunsetAt)}` : "no end date yet"}${d.link ? ` · ${p(d.link)}` : ""}`);
|
|
9320
|
+
}
|
|
9321
|
+
}
|
|
9322
|
+
|
|
8862
9323
|
/**
|
|
8863
9324
|
* `foldrun find <query>` — the ⌘K search: workspaces, agents, flows, tools,
|
|
8864
9325
|
* skills, knowledge, memory and runs across the account, ranked as the
|
|
@@ -9065,7 +9526,7 @@ function printPreview(p, rel) {
|
|
|
9065
9526
|
case "text":
|
|
9066
9527
|
case "html":
|
|
9067
9528
|
case "svg":
|
|
9068
|
-
|
|
9529
|
+
writeExact(`${p.text ?? p.html ?? p.svg ?? ""}`.replace(/\n?$/, "\n"));
|
|
9069
9530
|
if (p.truncated) out(c.dim("… (truncated)"));
|
|
9070
9531
|
return;
|
|
9071
9532
|
case "table":
|
|
@@ -9335,6 +9796,9 @@ export async function run(command, positional, flags, workspace, layout) {
|
|
|
9335
9796
|
return positional[0] === "test"
|
|
9336
9797
|
? toolTestCmd(positional, flags, layout)
|
|
9337
9798
|
: scaffoldCmd("tools", positional, flags, layout);
|
|
9799
|
+
case "hook":
|
|
9800
|
+
case "hooks":
|
|
9801
|
+
return hookCmd(positional, flags, layout);
|
|
9338
9802
|
case "invoke":
|
|
9339
9803
|
return invoke(positional[0], flags);
|
|
9340
9804
|
case "source":
|