foldrun 0.5.0 → 0.7.2

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.
@@ -99,6 +99,9 @@ function write(current, profiles) {
99
99
  * Remember a signed-in account. A second login to the same URL for the same
100
100
  * ACCOUNT replaces that profile rather than making a near-duplicate; a
101
101
  * different account on the same URL is a new one.
102
+ * @param {string} url
103
+ * @param {Record<string, any>} entry
104
+ * @param {{ makeDefault?: boolean, name?: string }} [opts]
102
105
  */
103
106
  export function saveCredential(url, entry, { makeDefault = true, name } = {}) {
104
107
  const key = normaliseUrl(url);
package/src/guide.mjs ADDED
@@ -0,0 +1,172 @@
1
+ // The rules a coding agent reads before it edits a foldrun account.
2
+ //
3
+ // Copied from what Next.js does (next/dist/server/lib/generate-agent-files):
4
+ // the docs ship inside the package (./docs, see scripts/sync-docs.mjs), and
5
+ // AGENTS.md carries one short managed block, between markers, telling the
6
+ // agent to read them before it writes anything; CLAUDE.md imports it with
7
+ // `@AGENTS.md`. The block is upserted in place, never duplicated, and a
8
+ // person's own text around it is never touched.
9
+ //
10
+ // One difference from Next.js: in a foldrun account AGENTS.md is also the
11
+ // account's own context, which every RUNNING agent is given. The runtime
12
+ // strips this block (foldrun-core runner.ts withoutAgentRules) so it
13
+ // reaches the coding tool and not the agents doing the work.
14
+ //
15
+ // Written by `foldrun init`, refreshed by `foldrun pull` and `foldrun
16
+ // guide`, and re-added by `foldrun check` when a coding agent is driving it
17
+ // and the block is missing or old — the way `next dev` does.
18
+
19
+ import fs from "node:fs";
20
+ import path from "node:path";
21
+
22
+ export const AGENT_RULES_START = "<!-- BEGIN:foldrun-agent-rules -->";
23
+ export const AGENT_RULES_END = "<!-- END:foldrun-agent-rules -->";
24
+ /** The block written before this design, into CLAUDE.md. Stripped on upsert. */
25
+ const LEGACY = /<!-- foldrun:guide v[0-9.]+ -->[\s\S]*?<!-- \/foldrun:guide -->\n?/g;
26
+ const CLAUDE_MD_CONTENT = "@AGENTS.md\n";
27
+
28
+ export function agentRulesBlock() {
29
+ return `${AGENT_RULES_START}
30
+
31
+ # Read foldrun's docs before you edit this folder
32
+
33
+ This is a foldrun account: agents, flows, tools and knowledge as markdown, run by foldrun. Its formats — frontmatter fields, step options, \`verify:\` prefixes, which folder a file goes in — are foldrun's own and may differ from your training data. Before you write or change an agent, flow, tool or skill, read the relevant guide: \`foldrun docs\` lists them and \`foldrun docs <page>\` prints one (start with \`foldrun docs coding-agents\`). They ship with the CLI, so they match the version you are driving. Check every edit with \`foldrun check\` — offline, no model calls. For a flow's shape, \`foldrun flow show <flow>\` draws it, and \`foldrun flow add\`, \`foldrun flow rm-step\`, \`foldrun flow dup-step\` and \`foldrun agent link\` make the same edits as the dashboard canvas (patterns, sub-agents, consults, \`ask\`). A script tool's own \`secrets: [NAMES]\` are granted with the tool; list on an agent only the secrets it uses directly. Paths to the workspace's own folders are written \`workspace/storage/…\` and \`workspace/state/…\` — in an agent's prose, a flow's options and a script's arguments — not \`../../\`, which still works but is the older spelling.
34
+
35
+ This block is for coding tools. It is written and re-added by the foldrun CLI, and the runtime strips it, so the agents that run here never see it; the rest of this file is the account's own context, which they do read. Committing the block with your work keeps the tree clean.
36
+
37
+ ${AGENT_RULES_END}`;
38
+ }
39
+
40
+ function read(file) {
41
+ try {
42
+ return fs.readFileSync(file, "utf8");
43
+ } catch {
44
+ return null;
45
+ }
46
+ }
47
+
48
+ function blockIn(text) {
49
+ if (!text) return null;
50
+ const start = text.indexOf(AGENT_RULES_START);
51
+ if (start === -1) return null;
52
+ const end = text.indexOf(AGENT_RULES_END, start);
53
+ return end === -1 ? null : text.slice(start, end + AGENT_RULES_END.length);
54
+ }
55
+
56
+ const eol = (text) => (text.includes("\r\n") ? "\r\n" : "\n");
57
+
58
+ /** The block replaced in place, or put first — ahead of the prose, after the frontmatter. */
59
+ function upsert(text, block) {
60
+ const nl = eol(text);
61
+ const b = block.replace(/\n/g, nl);
62
+ const current = blockIn(text);
63
+ if (current !== null) return text.replace(current, b);
64
+ const fm = /^---\r?\n[\s\S]*?\r?\n---\r?\n/.exec(text);
65
+ if (fm) {
66
+ const rest = text.slice(fm[0].length).replace(/^(\r?\n)+/, "");
67
+ return `${fm[0]}${nl}${b}${nl}${rest ? `${nl}${rest}` : ""}`;
68
+ }
69
+ return `${b}${nl}${text ? `${nl}${text}` : ""}`;
70
+ }
71
+
72
+ /** True when AGENTS.md (or CLAUDE.md) at `root` already carries the current block. */
73
+ export function hasCurrentAgentRules(root) {
74
+ const want = agentRulesBlock();
75
+ for (const name of ["AGENTS.md", "CLAUDE.md"]) {
76
+ const got = blockIn(read(path.join(root, name)));
77
+ if (got !== null && got.replace(/\r\n/g, "\n") === want) return true;
78
+ }
79
+ return false;
80
+ }
81
+
82
+ /**
83
+ * Put the block where a coding agent will find it, as Next.js does:
84
+ * - the file already hosting the block is rewritten in place;
85
+ * - else AGENTS.md when it exists (it always does in an account folder);
86
+ * - else CLAUDE.md when it exists;
87
+ * - else both are created.
88
+ * And CLAUDE.md imports AGENTS.md, since Claude Code reads CLAUDE.md: it is
89
+ * created as `@AGENTS.md` when missing, and an existing one that neither
90
+ * hosts the block nor imports AGENTS.md gets the import line appended.
91
+ * Returns what happened to each file: created | updated | unchanged | skipped.
92
+ */
93
+ export function writeAgentFiles(root) {
94
+ const agentsPath = path.join(root, "AGENTS.md");
95
+ const claudePath = path.join(root, "CLAUDE.md");
96
+ const block = agentRulesBlock();
97
+ let agents = read(agentsPath);
98
+ let claude = read(claudePath);
99
+ const result = { agentsMd: "skipped", claudeMd: "skipped" };
100
+
101
+ // The earlier design's block, in CLAUDE.md: gone before anything else.
102
+ if (claude !== null && LEGACY.test(claude)) {
103
+ claude = claude.replace(LEGACY, "").replace(/^\s+/, "");
104
+ fs.writeFileSync(claudePath, claude);
105
+ result.claudeMd = "updated";
106
+ }
107
+
108
+ const host = blockIn(claude) !== null && blockIn(agents) === null ? "claude" : agents !== null ? "agents" : claude !== null ? "claude" : "agents";
109
+ if (host === "agents") {
110
+ const next = upsert(agents ?? "", block);
111
+ if (next !== agents) {
112
+ fs.mkdirSync(root, { recursive: true });
113
+ fs.writeFileSync(agentsPath, next.endsWith("\n") ? next : `${next}\n`);
114
+ result.agentsMd = agents === null ? "created" : "updated";
115
+ } else result.agentsMd = "unchanged";
116
+ if (claude === null) {
117
+ fs.writeFileSync(claudePath, CLAUDE_MD_CONTENT);
118
+ result.claudeMd = "created";
119
+ } else if (!/^@AGENTS\.md\s*$/m.test(claude) && blockIn(claude) === null) {
120
+ fs.writeFileSync(claudePath, `${claude.replace(/\s*$/, "")}${claude.trim() ? "\n\n" : ""}@AGENTS.md\n`);
121
+ result.claudeMd = "updated";
122
+ } else if (result.claudeMd === "skipped") result.claudeMd = "unchanged";
123
+ } else {
124
+ const next = upsert(claude ?? "", block);
125
+ if (next !== claude) {
126
+ fs.writeFileSync(claudePath, next.endsWith("\n") ? next : `${next}\n`);
127
+ result.claudeMd = "updated";
128
+ } else if (result.claudeMd === "skipped") result.claudeMd = "unchanged";
129
+ }
130
+ return result;
131
+ }
132
+
133
+ /**
134
+ * Which coding agent is driving this process, or null. The variables the
135
+ * tools set for the commands they run; the same idea as Next.js's
136
+ * getAgentName, used for the same thing — re-adding the block when an agent
137
+ * is at work in a folder that lacks it.
138
+ */
139
+ export function codingAgent(env = process.env) {
140
+ if (env.CLAUDECODE || env.CLAUDE_CODE_ENTRYPOINT) return "claude-code";
141
+ if (env.CURSOR_AGENT || env.CURSOR_TRACE_ID) return "cursor";
142
+ if (env.CODEX_SANDBOX || env.CODEX_HOME || env.CODEX_MANAGED_BY_NPM) return "codex";
143
+ if (env.GEMINI_CLI) return "gemini";
144
+ if (env.AI_AGENT) return String(env.AI_AGENT);
145
+ return null;
146
+ }
147
+
148
+ // ------------------------------------------------------------------ docs
149
+
150
+ /** The docs that ship with this CLI. */
151
+ export function docsDir() {
152
+ return path.join(path.dirname(new URL(import.meta.url).pathname), "..", "docs");
153
+ }
154
+
155
+ /** Every page: its slug and its first heading. */
156
+ export function docPages() {
157
+ const dir = docsDir();
158
+ if (!fs.existsSync(dir)) return [];
159
+ return fs
160
+ .readdirSync(dir)
161
+ .filter((n) => n.endsWith(".md"))
162
+ .sort()
163
+ .map((n) => {
164
+ const text = fs.readFileSync(path.join(dir, n), "utf8");
165
+ return { slug: n.slice(0, -3), title: (/^# (.+)$/m.exec(text)?.[1] ?? n).trim() };
166
+ });
167
+ }
168
+
169
+ export function docPage(slug) {
170
+ const file = path.join(docsDir(), `${slug.replace(/\.md$/, "")}.md`);
171
+ return fs.existsSync(file) ? fs.readFileSync(file, "utf8") : null;
172
+ }