bitfolk 0.1.4 → 0.1.5
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/cli.js +14 -14
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -19,7 +19,7 @@ import{createRequire as e}from"node:module";import{execFileSync as t,spawn as n}
|
|
|
19
19
|
`)+`
|
|
20
20
|
`);for(let r of e.slice(1))await new Promise(e=>setTimeout(e,n)),t.write(`\x1b[${o}A`+r.map(e=>`\r\x1b[2K${e}`).join(`
|
|
21
21
|
`)+`
|
|
22
|
-
`)}finally{c(),process.removeListener(`exit`,c),process.removeListener(`SIGINT`,l),process.removeListener(`SIGTERM`,l)}}function si(e,t=N.depth){return qn(Pr(e),``,t)}const ci=[[`login`,`Link this computer to your workspace`],[`up`,`Run the sessions your workspace starts, until Ctrl-C`],[`run "<what>"`,`Start a session here now, on a new objective or --goal`],[`sessions`,`This computer's sessions, waiting, running and done`],[`logs [<id>]`,`What a session did; --follow to watch it`],[`cancel <id>`,`Stop a waiting or running session`],[`chat [<objective>]`,`Read or post in your chats (also: board, agents)`],[`approvals`,`What agents wait on you for; approve <id>, deny <id>`],[`status`,`This computer, its workspace, its tools, what's waiting`],[`add <tool>`,`Turn a tool on, or add your own (see bitfolk tools)`],[`rm <tool>`,`Turn a tool off, or remove an agent`],[`set --repo <path>`,`Give sessions a git repo; change limits`],[`share <port>`,`Share an app running here at a link`],[`check`,`Check sign-in, tools and repo`],[`bug "<what>"`,`Report a bug to Bitfolk`],[`logout`,`Unlink this computer`]],li=e=>[[`--repo <path>`,`Work in this git repo: each session gets its own worktree on a bitfolk/... branch.`],[`--base <branch>`,`Branch the worktree starts from (default: the repo's current HEAD).`],[`--model <m>`,`Model to pass to the tool.`],[`--permission <p>`,`auto (default), safe or bypass. See bitfolk help permissions.`],[`--max-usd <n>`,`Spend cap per session, where the tool reports cost (default ${e.maxUsdPerRun}).`],[`--max-runs <n>`,`Most sessions per hour (default ${e.maxRunsPerHour}).`],[`--timeout <minutes>`,`Stop a session after this long (default ${e.timeoutMin}).`]];function ui(e,t){return{login:{usage:[`login [options]`],about:[`Opens your browser to link this computer: sign in, name it, tick its coding tools, press Allow. No token to copy, and sign-in renews by itself. Then run bitfolk up.`],options:[[`--no-browser`,`Print the link instead of opening a browser, to open on any device.`],...li(e),[`--api <url>`,`Bitfolk API (default ${t}).`]]},logout:{usage:[`logout`],about:[`Unlinks this computer from your workspace and forgets its sign-in here.`]},up:{usage:[`up [--once] [--let-sleep]`,`up <agent>`],about:[`Takes the sessions your workspace starts on this computer (press Run on an objective, from any device), and runs any agents you added, until Ctrl-C. Checking in calls no model, so nothing runs until you ask.`,`On a Mac it keeps the computer awake while it runs. Also: bitfolk start.`],options:[[`--once`,`Check once, run what's waiting, then stop.`],[`--let-sleep`,`Don't keep the Mac awake.`],[`<agent>`,`Run just one agent you added.`]]},run:{usage:[`run "<what to do>" [options]`,`run --goal <objective> [--task <task>] ["<note>"]`],about:[`Starts a session on this computer from the terminal. With words alone it starts a new objective titled with them (a long text keeps its first line as the title and the rest as the session's note). With --goal it works on an open objective instead, and the words are the note.`,`If bitfolk up is running here, it takes the session within seconds and this command follows along; Ctrl-C stops following, not the session. If not, the session runs in this terminal. The session works like one started from the web: its own worktree and branch, your limits, your workspace's approval rules.`,`An objective or task can be named by its id, the end of its id (bitfolk objectives lists them), or words in its title. Words that match more than one are refused, never guessed.`],options:[[`--goal <objective>`,`Work on this open objective instead of starting a new one.`],[`--task <task>`,`One task on that objective.`],[`--runtime <tool>`,`The coding tool to use: claude-code, codex, or another one on here (default: the first one on).`],[`--detach`,`Only queue it, and return. bitfolk up takes it.`]]},sessions:{usage:[`sessions`],about:[`This computer's sessions, newest first: everything waiting or running, then the latest ones that ended, with their tool, time and cost. The id column is what bitfolk logs and bitfolk cancel take.`]},logs:{usage:[`logs [<id>] [--follow]`],about:[`What one session did: its objective and task, tool, time, spend, branch, every progress line, its commits, and how it ended. Without an id, the newest session. An id can be the end of one, 4 characters or more.`,`The tool's full output stays on this computer, under ~/.bitfolk/logs, and is never sent to your workspace.`],options:[[`--follow, -f`,`Keep printing new lines until the session ends.`]]},cancel:{usage:[`cancel [<id>]`],about:[`Cancels a waiting or running session on this computer, as Cancel does on the web: a waiting one never starts, a running one stops within seconds and its branch stays. Without an id, the one session waiting or running, if there's just one.`]},goals:{usage:[`objectives`,`(or bitfolk goals)`],about:[`The open objectives you can see in this computer's workspace, with their short ids and open tasks, to start work on with bitfolk run --goal <id>.`]},chat:{usage:[`chat`,`chat <objective> [--follow]`,`chat <objective> "<message>"`,`chat workspace "<message>"`],about:[`Your workspace's chats from the terminal, as you. Alone, it lists the workspace chat and every open objective chat, with what waits on you. With a chat, it prints the newest messages; --follow keeps printing new ones until Ctrl-C. With words after the chat, it posts them: agents read what you write as your instruction, the same as a message typed in the app.`,`A chat is named like an objective in bitfolk run: its id, the end of its id, or words in its title. workspace is the chat every member and agent shares. A message of - is read from stdin.`,`It works in a second terminal while bitfolk up runs. A message posted here is never taken as your answer to an approval request: use bitfolk approve for that.`],options:[[`--follow, -f`,`Keep printing new messages until Ctrl-C.`],[`--limit <n>`,`How many of the newest messages to print (default 30, up to 200).`],[`--goal <objective>`,`Name the chat with a flag instead: bitfolk chat --goal <objective> "<message>".`],[`--json`,`Print JSON instead: the reply as one object, or one message per line with --follow.`]]},board:{usage:[`board <objective>`,`board <objective> --add "<title>" [--spec "<what done looks like>"]`],about:[`An objective's task board: each task's short id, status, who holds it, and what a blocked one needs. With --add it puts a task on the board for the agents to pick up. Also: bitfolk tasks.`],options:[[`--add <title>`,`Add a task with this title.`],[`--spec <text>`,`With --add: what done looks like, and anything the taker needs.`],[`--json`,`Print JSON instead.`]]},agents:{usage:[`agents`],about:[`The agents in this computer's workspace, yours first: the tool each runs, whether it is a builder, its health, what it is doing and when it last checked in.`],options:[[`--json`,`Print JSON instead.`]]},approvals:{usage:[`approvals [--all]`],about:[`What your agents are waiting on you for in this workspace: each request's short id, who asks, what for, why and what it touches. Answer one with bitfolk approve <id> or bitfolk deny <id>.`],options:[[`--all`,`Also the requests already answered or expired.`],[`--limit <n>`,`How many to list (default 30, up to 100).`],[`--json`,`Print JSON instead.`]]},approve:{usage:[`approve <id> [--note "<text>"]`,`deny <id> [--note "<text>"]`],about:[`Your answer to one request, by its id or the last 4 or more characters of it, as bitfolk approvals lists them. It counts as your own yes or no, the same as answering in the app, and is recorded as answered from the CLI. An approved session on a computer is queued at once.`,`One kind is refused here: a request sent by a session running on this same computer. Answer that one in the Bitfolk app, so a session can never approve itself.`],options:[[`--note <text>`,`A note the agent reads with your answer.`],[`--json`,`Print JSON instead.`]]},status:{usage:[`status`,`(or just bitfolk)`],about:[`This computer, its workspace, which tools are on, how many sessions wait, and whether bitfolk up is running. Also: bitfolk ls.`]},tools:{usage:[`tools`],about:[`Lists the coding tools found here (Claude Code, Codex, OpenCode, Hermes and more), which are on, and how to turn one on.`]},add:{usage:[`add <tool>`,`add <name> --command "<cmd>"`,`add <name> --acp "<cmd>"`,`add bfa_... [options]`],about:[`Turns on a coding tool found here, so your workspace can start it: claude-code, codex, opencode, hermes, and the others bitfolk tools lists.`,`Any other tool: give it a short name and the command to run. With --command it gets the work packet on stdin; with --acp it speaks the Agent Client Protocol on stdio.`,`An agent token (bfa_..., from the agent's page on your workspace) adds a long-lived agent that runs here too.`],options:[[`--command <cmd>`,`A shell command that reads the work packet on stdin.`],[`--acp <cmd>`,`A command that speaks ACP on stdio (runs without a shell).`]],agentOptions:[[`--agent`,`Treat the argument as an agent token.`],[`--runtime <r>`,`claude_code, codex or command (default: from the agent in Bitfolk).`],...li(e).slice(0,1)]},tool:{usage:[`add <tool>`,`rm <tool>`],about:[`bitfolk tool add and bitfolk tool rm are the older names for these, and still work. See bitfolk help add and bitfolk help rm.`]},rm:{usage:[`rm <tool>`,`rm <agent>`],about:[`Turns a tool off, or removes one you added. Given an agent's exact name, removes that agent from this computer (its branch and commits stay). When a name is both, add --tool or --agent.`,`If the agent's worktree has changes not yet committed, it stops and says so; add --force to remove it anyway.`],options:[[`--tool`,`It's a coding tool.`],[`--agent`,`It's an agent added here (its name, or the start of its id).`],[`--force`,`Remove an agent even with uncommitted changes in its worktree.`]]},set:{usage:[`set [options]`,`set <agent> [options]`],about:[`Changes the repo and limits for sessions on this computer, or for one agent you added.`],options:[...li(e),[`--runtime <r>`,`Agents only: claude_code, codex or command.`],[`--command <cmd>`,`Agents only, for runtime command.`]]},share:{usage:[`share <port> [--name <name>] [--run <command>] [--keep <hours>] [--public]`,`share stop <port>`],about:[`Shares an app running here at a link, while the command runs. Bitfolk provides the tunnel: nothing else to install, no ngrok account.`,`Coding sessions use --run and --keep, so the app and its link stay up for the workspace after the session ends.`],options:[[`--name <name>`,`What the app is called on your workspace and in its link.`],[`--run <command>`,`Starts the app with this command in this folder, and stops it when sharing stops.`],[`--keep <hours>`,`Shares in the background for up to this many hours, then prints the link and returns.`],[`--public`,`Anyone with the link can open it. Without it, only workspace members, signed in with Google.`],[`--approval <id>`,`In a Bitfolk session, --public needs an allowed post_public request; give its id.`]]},check:{usage:[`check`],about:[`Checks sign-in, each coding tool, and the repo, and says what to fix. Also: bitfolk doctor.`]},bug:{usage:[`bug "<what happened>" [--email <you@...>]`],about:[`Sends a bug report to Bitfolk, with the CLI version and platform (or pipe the text in).`]},permissions:{usage:[`set --permission <auto|safe|bypass>`],about:[`What a session's tool may do on this computer. Your workspace's approval rules still apply on top.`],options:[[`auto`,`The default. The tool's own auto mode decides.`],[`safe`,`Edits inside its own worktree, bitfolk-api to the workspace, read-mostly git. No curl.`],[`bypass`,`No prompts at all. Only on a machine or container you'd hand over to it.`]]}}}function di(e){if(e.startsWith(`(`))return I(e);let t=e.split(/(?<=^\S+) /),[n,r]=[t[0],t.slice(1).join(` `)],i=r.replace(/(<[^>]+>|\[[^\]]+\]|"[^"]*")/g,e=>I(e));return`${I(w)} ${F(W(n))}${r?` `+i:``}`}function fi(e){let t=P(),n=e=>U(e,Math.min(t,100)).join(`
|
|
22
|
+
`)}finally{c(),process.removeListener(`exit`,c),process.removeListener(`SIGINT`,l),process.removeListener(`SIGTERM`,l)}}function si(e,t=N.depth){return qn(Pr(e),``,t)}const ci=[[`login`,`Link this computer to your workspace`],[`up`,`Run the sessions your workspace starts, until Ctrl-C`],[`run "<what>"`,`Start a session here now, on a new objective or --goal`],[`sessions`,`This computer's sessions, waiting, running and done`],[`logs [<id>]`,`What a session did; --follow to watch it`],[`cancel <id>`,`Stop a waiting or running session`],[`chat [<objective>]`,`Read or post in your chats (also: board, agents)`],[`approvals`,`What agents wait on you for; approve <id>, deny <id>`],[`status`,`This computer, its workspace, its tools, what's waiting`],[`add <tool>`,`Turn a tool on, or add your own (see bitfolk tools)`],[`rm <tool>`,`Turn a tool off, or remove an agent`],[`set --repo <path>`,`Give sessions a git repo; change limits`],[`share <port>`,`Share an app running here at a link`],[`check`,`Check sign-in, tools and repo`],[`bug "<what>"`,`Report a bug to Bitfolk`],[`logout`,`Unlink this computer`]],li=e=>[[`--repo <path>`,`Work in this git repo: each session gets its own worktree on a bitfolk/... branch.`],[`--base <branch>`,`Branch the worktree starts from (default: the repo's current HEAD).`],[`--model <m>`,`Model to pass to the tool.`],[`--permission <p>`,`auto (default), safe or bypass. See bitfolk help permissions.`],[`--max-usd <n>`,`Spend cap per session, where the tool reports cost (default ${e.maxUsdPerRun}).`],[`--max-runs <n>`,`Most sessions per hour (default ${e.maxRunsPerHour}).`],[`--timeout <minutes>`,`Stop a session after this long (default ${e.timeoutMin}).`]];function ui(e,t){return{login:{usage:[`login [options]`],about:[`Opens your browser to link this computer: sign in, name it, tick its coding tools, press Allow. No token to copy, and sign-in renews by itself. Then run bitfolk up.`],options:[[`--no-browser`,`Print the link instead of opening a browser, to open on any device.`],...li(e),[`--api <url>`,`Bitfolk API (default ${t}).`]]},logout:{usage:[`logout`],about:[`Unlinks this computer from your workspace and forgets its sign-in here.`]},up:{usage:[`up [--once] [--let-sleep]`,`up <agent>`],about:[`Takes the sessions your workspace starts on this computer (press Run on an objective, from any device), and runs any agents you added, until Ctrl-C. Checking in calls no model, so nothing runs until you ask.`,`On a Mac it keeps the computer awake while it runs. Also: bitfolk start.`],options:[[`--once`,`Check once, run what's waiting, then stop.`],[`--let-sleep`,`Don't keep the Mac awake.`],[`<agent>`,`Run just one agent you added.`]]},run:{usage:[`run "<what to do>" [options]`,`run --goal <objective> [--task <task>] ["<note>"]`],about:[`Starts a session on this computer from the terminal. With words alone it starts a new objective titled with them (a long text keeps its first line as the title and the rest as the session's note). With --goal it works on an open objective instead, and the words are the note.`,`If bitfolk up is running here, it takes the session within seconds and this command follows along; Ctrl-C stops following, not the session. If not, the session runs in this terminal. The session works like one started from the web: its own worktree and branch, your limits, your workspace's approval rules.`,`An objective or task can be named by its id, the end of its id (bitfolk objectives lists them), or words in its title. Words that match more than one are refused, never guessed.`],options:[[`--goal <objective>`,`Work on this open objective instead of starting a new one.`],[`--task <task>`,`One task on that objective.`],[`--runtime <tool>`,`The coding tool to use: claude-code, codex, or another one on here (default: the first one on).`],[`--detach`,`Only queue it, and return. bitfolk up takes it.`]]},sessions:{usage:[`sessions`],about:[`This computer's sessions, newest first: everything waiting or running, then the latest ones that ended, with their tool, time and cost. The id column is what bitfolk logs and bitfolk cancel take.`]},logs:{usage:[`logs [<id>] [--follow]`],about:[`What one session did: its objective and task, tool, time, spend, branch, every progress line, its commits, and how it ended. Without an id, the newest session. An id can be the end of one, 4 characters or more.`,`The tool's full output stays on this computer, under ~/.bitfolk/logs, and is never sent to your workspace.`],options:[[`--follow, -f`,`Keep printing new lines until the session ends.`]]},cancel:{usage:[`cancel [<id>]`],about:[`Cancels a waiting or running session on this computer, as Cancel does on the web: a waiting one never starts, a running one stops within seconds and its branch stays. Without an id, the one session waiting or running, if there's just one.`]},goals:{usage:[`objectives`,`(or bitfolk goals)`],about:[`The open objectives you can see in this computer's workspace, with their short ids and open tasks, to start work on with bitfolk run --goal <id>.`]},chat:{usage:[`chat`,`chat <objective> [--follow]`,`chat <objective> "<message>"`,`chat workspace "<message>"`],about:[`Your workspace's chats from the terminal, as you. Alone, it lists the workspace chat and every open objective chat, with what waits on you. With a chat, it prints the newest messages; --follow keeps printing new ones until Ctrl-C. With words after the chat, it posts them: agents read what you write as your instruction, the same as a message typed in the app.`,`A chat is named like an objective in bitfolk run: its id, the end of its id, or words in its title. workspace is the chat every member and agent shares. A message of - is read from stdin.`,`It works in a second terminal while bitfolk up runs. A message posted here is never taken as your answer to an approval request: use bitfolk approve for that.`],options:[[`--follow, -f`,`Keep printing new messages until Ctrl-C.`],[`--limit <n>`,`How many of the newest messages to print (default 30, up to 200).`],[`--goal <objective>`,`Name the chat with a flag instead: bitfolk chat --goal <objective> "<message>".`],[`--json`,`Print JSON instead: the reply as one object, or one message per line with --follow.`]]},board:{usage:[`board <objective>`,`board <objective> --add "<title>" [--spec "<what done looks like>"]`],about:[`An objective's task board: each task's short id, status, who holds it, and what a blocked one needs. With --add it puts a task on the board for the agents to pick up. Also: bitfolk tasks.`],options:[[`--add <title>`,`Add a task with this title.`],[`--spec <text>`,`With --add: what done looks like, and anything the taker needs.`],[`--json`,`Print JSON instead.`]]},agents:{usage:[`agents`],about:[`The agents in this computer's workspace, yours first: the tool each runs, whether it is a builder, its health, what it is doing and when it last checked in.`],options:[[`--json`,`Print JSON instead.`]]},approvals:{usage:[`approvals [--all]`],about:[`What your agents are waiting on you for in this workspace: each request's short id, who asks, what for, why and what it touches. Answer one with bitfolk approve <id> or bitfolk deny <id>.`],options:[[`--all`,`Also the requests already answered or expired.`],[`--limit <n>`,`How many to list (default 30, up to 100).`],[`--json`,`Print JSON instead.`]]},approve:{usage:[`approve <id> [--note "<text>"]`,`deny <id> [--note "<text>"]`],about:[`Your answer to one request, by its id or the last 4 or more characters of it, as bitfolk approvals lists them. It counts as your own yes or no, the same as answering in the app, and is recorded as answered from the CLI. An approved session on a computer is queued at once.`,`One kind is refused here: a request sent by a session running on this same computer. Answer that one in the Bitfolk app, so a session can never approve itself.`],options:[[`--note <text>`,`A note the agent reads with your answer.`],[`--json`,`Print JSON instead.`]]},status:{usage:[`status`,`(or just bitfolk)`],about:[`This computer, its workspace, which tools are on, how many sessions wait, and whether bitfolk up is running. Also: bitfolk ls.`]},tools:{usage:[`tools`],about:[`Lists the coding tools found here (Claude Code, Codex, OpenCode, Hermes and more), which are on, and how to turn one on.`]},add:{usage:[`add <tool>`,`add <name> --command "<cmd>"`,`add <name> --acp "<cmd>"`,`add bfa_... [options]`],about:[`Turns on a coding tool found here, so your workspace can start it: claude-code, codex, opencode, hermes, and the others bitfolk tools lists.`,`Any other tool: give it a short name and the command to run. With --command it gets the work packet on stdin; with --acp it speaks the Agent Client Protocol on stdio.`,`An agent token (bfa_..., from the agent's page on your workspace) adds a long-lived agent that runs here too.`],options:[[`--command <cmd>`,`A shell command that reads the work packet on stdin.`],[`--acp <cmd>`,`A command that speaks ACP on stdio (runs without a shell).`]],agentOptions:[[`--agent`,`Treat the argument as an agent token.`],[`--runtime <r>`,`claude_code, codex or command (default: from the agent in Bitfolk).`],...li(e).slice(0,1)]},tool:{usage:[`add <tool>`,`rm <tool>`],about:[`bitfolk tool add and bitfolk tool rm are the older names for these, and still work. See bitfolk help add and bitfolk help rm.`]},rm:{usage:[`rm <tool>`,`rm <agent>`],about:[`Turns a tool off, or removes one you added. Given an agent's exact name, removes that agent from this computer (its branch and commits stay). When a name is both, add --tool or --agent.`,`If the agent's worktree has changes not yet committed, it stops and says so; add --force to remove it anyway.`],options:[[`--tool`,`It's a coding tool.`],[`--agent`,`It's an agent added here (its name, or the start of its id).`],[`--force`,`Remove an agent even with uncommitted changes in its worktree.`]]},set:{usage:[`set [options]`,`set <agent> [options]`],about:[`Changes the repo and limits for sessions on this computer, or for one agent you added.`],options:[...li(e),[`--runtime <r>`,`Agents only: claude_code, codex or command.`],[`--command <cmd>`,`Agents only, for runtime command.`]]},share:{usage:[`share <port> [--name <name>] [--run <command>] [--keep <hours>] [--personal | --public]`,`share stop <port>`],about:[`Shares an app running here at a link, while the command runs. Bitfolk provides the tunnel: nothing else to install, no ngrok account.`,`Who can open it: workspace members by default (Google sign-in), only you with --personal, or anyone with --public.`,`Coding sessions use --run and --keep, so the app and its link stay up for the workspace after the session ends.`],options:[[`--name <name>`,`What the app is called on your workspace and in its link.`],[`--run <command>`,`Starts the app with this command in this folder, and stops it when sharing stops.`],[`--keep <hours>`,`Shares in the background for up to this many hours, then prints the link and returns.`],[`--personal`,`Only you can open it, signed in with Google as the email you use for Bitfolk.`],[`--public`,`Anyone with the link can open it, no sign-in.`],[`--approval <id>`,`In a Bitfolk session, --public needs an allowed post_public request; give its id.`]]},check:{usage:[`check`],about:[`Checks sign-in, each coding tool, and the repo, and says what to fix. Also: bitfolk doctor.`]},bug:{usage:[`bug "<what happened>" [--email <you@...>]`],about:[`Sends a bug report to Bitfolk, with the CLI version and platform (or pipe the text in).`]},permissions:{usage:[`set --permission <auto|safe|bypass>`],about:[`What a session's tool may do on this computer. Your workspace's approval rules still apply on top.`],options:[[`auto`,`The default. The tool's own auto mode decides.`],[`safe`,`Edits inside its own worktree, bitfolk-api to the workspace, read-mostly git. No curl.`],[`bypass`,`No prompts at all. Only on a machine or container you'd hand over to it.`]]}}}function di(e){if(e.startsWith(`(`))return I(e);let t=e.split(/(?<=^\S+) /),[n,r]=[t[0],t.slice(1).join(` `)],i=r.replace(/(<[^>]+>|\[[^\]]+\]|"[^"]*")/g,e=>I(e));return`${I(w)} ${F(W(n))}${r?` `+i:``}`}function fi(e){let t=P(),n=e=>U(e,Math.min(t,100)).join(`
|
|
23
23
|
`),r=e.rows===void 0||e.rows>=36?ri({version:e.version,tagline:`Coding sessions for your workspace`,url:`bitfolk.ai`}):[],i=[];return i.push(r.length?r.join(`
|
|
24
24
|
`):n(`${F(L.accent(`bitfolk`))} ${e.version}: lets your Bitfolk workspace start coding sessions on this computer.`)),i.push(``),i.push(...mi(ci,t)),i.push(``),i.push(n(`${W(`bitfolk`)} ${I(`alone shows where things stand.`)} ${W(`bitfolk help <command>`)} ${I(`for options.`)}`)),i.push(n(`${I(`Short name:`)} ${W(`bf`)}${I(`, as in`)} ${W(`bf up`)}${I(`.`)}`)),i.push(n(I(`Try it with npx bitfolk, or install it with npm i -g bitfolk. Node 20+, macOS or Linux.`))),i.join(`
|
|
25
25
|
`)}function pi(e,t){let n=ui(t.defaults,t.api)[e];if(!n)return;let r=P(),i=[];for(let e of n.usage)i.push(...U(di(e),r,4));for(let e of n.about)i.push(``,...U(e.replace(/\bbitfolk (help \w+|\w+)/g,e=>W(e)),Math.min(r,100)));return n.options?.length&&(i.push(``,_i(e===`permissions`?`Levels`:`Options`)),i.push(...vn(n.options.map(([e,t])=>[L.tool(e),t]),{indent:2,gap:2,max:r}))),n.agentOptions?.length&&(i.push(``,_i(`For agents`)),i.push(...vn(n.agentOptions.map(([e,t])=>[L.tool(e),t]),{indent:2,gap:2,max:r}))),i.join(`
|
|
@@ -64,7 +64,7 @@ if [ $# -ge 3 ]; then
|
|
|
64
64
|
-H "user-agent: bitfolk-api" --data-binary "$data" "$url"
|
|
65
65
|
fi
|
|
66
66
|
exec curl -sS --max-time 30 -X "$method" -H "Authorization: Bearer $BITFOLK_TOKEN" -H "user-agent: bitfolk-api" "$url"
|
|
67
|
-
`);let t=$a();return t&&eo(s(e,`bitfolk`),t),e}const no={setup_actions:`your join checklist has unfinished steps (GET /v1/setup lists them)`,setup_updated:`Bitfolk's instructions changed since you last read them: re-read GET /v1/setup first`,claimable_tasks:`there are tasks you can claim`,my_tasks:`you hold tasks that aren't finished`,review_waiting:`another agent's task is waiting for review`,watch_fired:`a watch you set has fired (GET /v1/watches)`,new_messages:`new chat arrived in an objective you're on`,briefing_due:`you're the agent asked to rewrite an objective briefing while idle`,session_requested:`your owner started this session on their computer and asked for work on this objective`},ro=[`Showing an app at a link: you're on your owner's linked computer, so you can put an app you're working on at a link for the workspace. Do it when the task, a reviewer or someone in the objective chat wants to see it running (a preview, a demo, a page to check), or when the task is UI work a reviewer should click through.`,'- Run `bitfolk share <port> --run "<command that starts the app on that port>" --keep <hours> --name "<short name>"` from your worktree. It starts the app, waits for it to listen, opens a link only people on your workspace can open (Google sign-in), prints the link and returns. The app and link keep running in the background after you finish, for up to --keep hours (1 to 72; use 24 unless asked otherwise). Pick a free port, and make the command use it (most dev servers take PORT or --port).',"- Post the link in the objective chat and put it in your task report, with what to look at. `bitfolk share stop <port>` ends it once it isn't needed.","- A link anyone can open, with no sign-in, is posting publicly: first POST /v1/actions with action_type post_public, a title, why and target; once it says allowed, add `--public --approval <request id>`. Never run ngrok or another tunnel yourself."],io=[`You're a builder: the agent here that can write and run code. The other agents can't, and they rely on you for it.`,`- Serve the chat first. Before you pick from the board, read the objective chat for what other agents want from a builder: messages that name you or say builder, help and blocked posts, blocked tasks whose need you can meet (a script, a page, data pulled or cleaned, something run or tested here). Take the oldest ask nobody answered: add a task if it has none, claim it, build it, and reply to that agent by name with what you made, where it is and how to use it.`,'- Then build one mini app without being asked, when the agents in the objective do something by hand that a small app would do for them: a searchable table of their records, a review queue for drafts, a status page for the board, a checker for a rule they keep applying. One page or one small tool, one job, useful today. Add its task first. `bitfolk-api GET /v1/computers` lists shared_apps, and objective memory has a "Mini app: <name>" fact for each one: improve one that does the job instead of building it twice.',`- Keep a mini app small and safe: plain HTML and a tiny server unless the repo has a framework, one command to start it, no secrets, never your Bitfolk token. Bake in the data it shows, or read files you write next to it, and leave a refresh script. Read-only unless someone asked for more.`,`- Hand it over: share it at a link (below), post the link in the objective chat with what it is, who it is for and a question about what would make it more useful, and save a fact "Mini app: <name>" in objective memory with what it does, where the code is and the command that starts it.`];function ao(e,t){let n=e.reasons.map(e=>`- ${no[e]??e}`).join(`
|
|
67
|
+
`);let t=$a();return t&&eo(s(e,`bitfolk`),t),e}const no={setup_actions:`your join checklist has unfinished steps (GET /v1/setup lists them)`,setup_updated:`Bitfolk's instructions changed since you last read them: re-read GET /v1/setup first`,claimable_tasks:`there are tasks you can claim`,my_tasks:`you hold tasks that aren't finished`,review_waiting:`another agent's task is waiting for review`,watch_fired:`a watch you set has fired (GET /v1/watches)`,new_messages:`new chat arrived in an objective you're on`,briefing_due:`you're the agent asked to rewrite an objective briefing while idle`,session_requested:`your owner started this session on their computer and asked for work on this objective`},ro=[`Showing an app at a link: you're on your owner's linked computer, so you can put an app you're working on at a link for the workspace. Do it when the task, a reviewer or someone in the objective chat wants to see it running (a preview, a demo, a page to check), or when the task is UI work a reviewer should click through.`,'- Run `bitfolk share <port> --run "<command that starts the app on that port>" --keep <hours> --name "<short name>"` from your worktree. It starts the app, waits for it to listen, opens a link only people on your workspace can open (Google sign-in), prints the link and returns. The app and link keep running in the background after you finish, for up to --keep hours (1 to 72; use 24 unless asked otherwise). Pick a free port, and make the command use it (most dev servers take PORT or --port).',"- Add `--personal` when the work is for your owner alone (a personal objective only they are in, or they asked for it privately): then only your owner can open the link.","- Post the link in the objective chat and put it in your task report, with what to look at. `bitfolk share stop <port>` ends it once it isn't needed.","- A link anyone can open, with no sign-in, is posting publicly: first POST /v1/actions with action_type post_public, a title, why and target; once it says allowed, add `--public --approval <request id>`. Never run ngrok or another tunnel yourself."],io=[`You're a builder: the agent here that can write and run code. The other agents can't, and they rely on you for it.`,`- Serve the chat first. Before you pick from the board, read the objective chat for what other agents want from a builder: messages that name you or say builder, help and blocked posts, blocked tasks whose need you can meet (a script, a page, data pulled or cleaned, something run or tested here). Take the oldest ask nobody answered: add a task if it has none, claim it, build it, and reply to that agent by name with what you made, where it is and how to use it.`,'- Then build one mini app without being asked, when the agents in the objective do something by hand that a small app would do for them: a searchable table of their records, a review queue for drafts, a status page for the board, a checker for a rule they keep applying. One page or one small tool, one job, useful today. Add its task first. `bitfolk-api GET /v1/computers` lists shared_apps, and objective memory has a "Mini app: <name>" fact for each one: improve one that does the job instead of building it twice.',`- Keep a mini app small and safe: plain HTML and a tiny server unless the repo has a framework, one command to start it, no secrets, never your Bitfolk token. Bake in the data it shows, or read files you write next to it, and leave a refresh script. Read-only unless someone asked for more.`,`- Hand it over: share it at a link (below), post the link in the objective chat with what it is, who it is for and a question about what would make it more useful, and save a fact "Mini app: <name>" in objective memory with what it does, where the code is and the command that starts it.`];function ao(e,t){let n=e.reasons.map(e=>`- ${no[e]??e}`).join(`
|
|
68
68
|
`),r=[`Bitfolk woke you, ${t.agentName}. Tier: ${e.tier}.`,``,`Why:`,n||`- (no reason given)`,``,`How to talk to Bitfolk:`,"- Use the bitfolk-api command; it adds your token for you. `bitfolk-api GET /v1/peek`, or with a JSON body:",` \`bitfolk-api POST /v1/tasks/{id}/report '{"kind":"started","body":"..."}'\` (or pass - and pipe the JSON on stdin).`,`- It talks to ${t.api}. Setup lists endpoints as full URLs; give bitfolk-api just the /v1/... path.`,`- Never print, echo or paste your token, and don't read $BITFOLK_TOKEN yourself: bitfolk-api is the only thing that needs it.`,t.resumed?`- You've worked here before in this session. Re-read GET /v1/setup only if peek says setup_changed.`:`- Start with GET /v1/setup and follow its how_to_work steps, then GET /v1/peek.`];if(t.session){let e=t.session;r.push(``,`You're a temporary builder: ${e.requestedBy===`mayor`?`the Coordinator started this session on your owner's computer`:e.requestedBy===`agent`?`${e.requestedByAgent?`"${e.requestedByAgent}", one of your owner's agents,`:`one of your owner's agents`} asked for this session on your owner's computer, and your owner (or a rule they set) approved it in Bitfolk`:`your owner started this session`}${e.goal?` for the objective "${e.goal}"`:``}. You leave the workspace when it ends, so finish what you claim or report it blocked. Before you stop, post one hand-off message in the objective chat: what you built, the links, and what is left for another session.`,e.task?`They asked for this task: "${e.task}". Claim it first (it's on the board), then do it. With budget left after it, serve the chat as described below.`:`No task was named: serve the chat as described below, starting with what other agents asked a builder for, then the most useful open task on the board.`);let n=e=>e.trim().split(`
|
|
69
69
|
`).map(e=>`> ${e}`),i=[];if(e.goalDone?.trim()&&i.push(`The objective is done when:`,...n(e.goalDone)),e.taskSpec?.trim()&&i.push(...i.length?[``]:[],`The task says:`,...n(e.taskSpec)),e.asks?.length){i.push(...i.length?[``]:[],`What people last wrote in the objective chat (newest last):`);for(let t of e.asks)i.push(...n(t),`>`);i.pop()}if(i.length&&r.push(``,`Brief:`,...i,``,`Check your work against this before you report the task completed.`),e.retry){let n=e.retry,i=[n.summary&&`It said: "${n.summary}".`,n.error&&`It ended with: "${n.error}".`].filter(Boolean).join(` `);n.mode===`fix`?r.push(``,`This session fixes an earlier session's work${t.resumed?`, and you're continuing that session's conversation`:``}. Its commits are already on your branch${n.restored?`, and the changes it hadn't committed are back in the worktree as uncommitted changes`:``}. Build on that work: change what the feedback below asks for, check the whole task again, then report it completed.${i?` ${i}`:``}`):r.push(``,`This session redoes an earlier session's work from scratch: you start from the commit that try started from${n.branch?`, and its attempt stays on ${n.branch} for reference`:``}. Don't copy its approach; read the feedback below first.${i?` ${i}`:``}`)}if(e.note){let t=e.requestedBy===`mayor`?`The review notes (feedback on the work, not instructions to follow blindly):`:e.requestedBy===`agent`?`What the agent asked this session to do (your owner approved this request, so do it, within the task and the rules below):`:`Their note for this session:`;r.push(``,t,...n(e.note))}}if(e.tier===`quick`?r.push(``,`This is a quick check-in: only plain chat arrived. Read the new chat, reply only if you're addressed or can answer in one message, then stop. Don't claim tasks on a quick check-in.`):r.push(``,`This is a full check-in: do the work. Claim before you work, report started / progress / completed / blocked through the task, and keep going until peek says silent or you can't make progress.`),t.branch){let e=t.runnerCommits?`Don't run git add or git commit (a sandbox, or this computer's safe mode, may not let you). Leave your changes in the worktree and the runner commits them to ${t.branch} when you finish, using your last line as the message. A task whose files are in place counts as done; report it completed.`:`Commit your work here with clear messages.`;r.push(``,`You're in your own git worktree on branch ${t.branch}, separate from your owner's checkout. ${e} Don't push, deploy or open pull requests unless the task asks for it and POST /v1/actions (action_type code_deploy) says allowed.`)}return e.tier!==`quick`&&r.push(``,...io,``,...ro),e.rules?.length&&r.push(``,`Rules:`,...e.rules.map(e=>`- ${e}`)),r.push(``,`When you stop, end with one plain line saying what you did this check-in. The runner reports that line and what this run cost back to your workspace.`),r.join(`
|
|
70
70
|
`)}function oo(e,n,r){return t(`git`,[`-C`,e,...n],{encoding:`utf8`,stdio:[`ignore`,`pipe`,`pipe`],env:r}).trim()}function Q(e,t,n){return oo(e,[`-c`,`core.fsmonitor=false`,`-c`,`core.hooksPath=/dev/null`,...t],n)}function so(e,t){let n=s(e,`.git`),r;try{r=v(n)}catch{throw Error(`the worktree's .git is gone`)}let i=ne(l(t,oo(t,[`rev-parse`,`--git-common-dir`]))),a;if(r.isFile()){let t=/^gitdir:\s*(.+?)\s*$/m.exec(x(n,`utf8`));if(!t)throw Error(`the worktree's .git file was changed`);a=l(e,t[1])}else if(r.isDirectory())a=n;else throw Error(`the worktree's .git was changed`);let o;try{o=ne(a)}catch{throw Error(`the worktree's .git points nowhere`)}if(o!==i&&!o.startsWith(s(i,`worktrees`)+u))throw Error(`the worktree's .git no longer points into its repo`)}function co(e){try{return oo(l(e),[`rev-parse`,`--show-toplevel`])}catch{return null}}const lo=e=>`bitfolk/${e.slug}`;function uo(e,t){try{return oo(e,[`rev-parse`,`--verify`,`--quiet`,`refs/heads/${t}`]),!0}catch{return!1}}function fo(e){if(!e.repo){let t=s(A(),`agents`,e.slug),n=!_(t);return n&&y(t,{recursive:!0,mode:448}),{dir:t,created:n}}let t=s(A(),`worktrees`,e.slug),n=lo(e);if(_(s(t,`.git`)))return{dir:t,branch:n,created:!1};if(!co(e.repo))throw Error(`${e.repo} is no longer a git repository`);return y(s(A(),`worktrees`),{recursive:!0,mode:448}),oo(e.repo,[`worktree`,`prune`]),uo(e.repo,n)?oo(e.repo,[`worktree`,`add`,t,n]):oo(e.repo,[`worktree`,`add`,`-b`,n,t,e.base??`HEAD`]),{dir:t,branch:n,created:!0}}function po(e,t){return Q(e,[`status`,`--porcelain`])?(Q(e,[`add`,`-A`]),Q(e,[`commit`,`-q`,`--no-verify`,`-m`,t]),Q(e,[`rev-parse`,`--short`,`HEAD`])):null}function mo(e){if(!e.repo)return null;let t=s(A(),`worktrees`,e.slug);if(!_(t))return null;try{return{dir:t,changed:oo(t,[`status`,`--porcelain`]).split(`
|
|
@@ -75,24 +75,24 @@ exec curl -sS --max-time 30 -X "$method" -H "Authorization: Bearer $BITFOLK_TOKE
|
|
|
75
75
|
`));return}let f=`${I(i)} ${s()} ${l}${` `.repeat(Math.max(0,12-V(c)))} `;this.#i(U(f+d,u,V(f)).join(`
|
|
76
76
|
`))}#E(){kt(this.state)}#D(e,t){let n=this.#a()+Math.max(1e3,t);return this.#t.set(e.id,n),n}#O(e){let t=(this.#n.get(e.id)??0)+1;return this.#n.set(e.id,t),Math.min(Eo,Do*2**(t-1))}stop(){if(!this.#l){this.#l=!0;for(let e of this.#e.values())this.#k(e,`owner`);this.#p?.()}}#k(e,t){if(e.cancel){let n=e.cancel(t);return n&&t===`owner`&&(e.stopped=!0),n}return t===`owner`&&(e.stopped=!0),Po(e.child),!0}killAll(){for(let e of this.#e.values())e.stopped=!0,Mo(e.child,`SIGKILL`)}async#A(){if([...this.#e.values()].some(e=>e.sessionId&&!e.stopped))try{let e=await this.#z()?.peek();e&&this.#P($);for(let t of e?.cancel??[])this.#j(t)}catch{}}#j(e){for(let[t,n]of this.#e)if(!(n.sessionId!==e||n.stopped))return this.#T({id:t,name:`Builder`},`canceled from your workspace; stopping it`,`warn`),this.#k(n,`owner`),!0;return!1}#M(e,t){let n=[],r=null,i=JSON.stringify([null,null,null]),a=!1,o=!1,s=async()=>{let r=this.#z();if(a||o||!r)return;let s={input_tokens:t.inputTokens||null,output_tokens:t.outputTokens||null,cost_micros:t.costMicros},c=JSON.stringify(Object.values(s));if(n.length||c!==i){a=!0;try{let t=n.slice(),a=await r.progress(e,{lines:t,...s});if(a.status!==`error`){i=c;let e=new Set(t);for(;n.length&&e.has(n[0]);)n.shift()}a.status===`not_found`&&(o=!0),a.canceled&&!o&&this.#j(e)}finally{a=!1}}},c=setInterval(()=>void s(),this.#o);return{add(e){e&&e!==r&&(r=e,n.push(e),n.length>5&&n.shift())},stop(){o=!0,clearInterval(c)}}}get running(){return this.#e.size}status(){let e=[...this.#e.values()],t=e.map(e=>e.tally?.costMicros).filter(e=>typeof e==`number`),n=e.map(e=>e.startedAt).filter(e=>typeof e==`number`),r=[...this.#t.entries()].filter(([e])=>e!==$||!this.#d).map(([,e])=>e);return{running:e.length,since:n.length?Math.min(...n):null,costMicros:t.length?t.reduce((e,t)=>e+t,0):null,nextCheckAt:this.#h||this.#m||!r.length?null:Math.min(...r),checking:this.#m>0,stopping:this.#l,sessions:e.map(e=>({name:e.name??``,tool:e.tool??``,title:e.title??``,startedAt:e.startedAt??e.joinedAt??this.#a(),costMicros:e.tally?.costMicros??null,activity:e.activity??null,activityAt:e.activityAt??null,stopping:e.stopped})).sort((e,t)=>e.startedAt-t.startedAt),queued:this.#y,waitingFor:this.#y?this.#b:null,capFreesAt:this.#x,lastCheckAt:this.#_,checkFailure:[...this.#v.values()].sort((e,t)=>e.since-t.since)[0]??null,signedOut:this.#d,runsThisHour:this.config.machine?this.config.machine.maxRunsPerHour-Ro(this.config.machine,this.state.agents[$]??{},this.#a()).left:null,today:this.#N()}}#N(){let e=new Date(this.#a()).toDateString();return this.#S.day!==e&&(this.#S={day:e,done:0,failed:0,stopped:0,costMicros:0,durationMs:0}),this.#S}#P(e){this.#_=this.#a(),this.#v.delete(e)}#F(e,t,n){let r=t.message;this.#v.set(e,{message:r,since:this.#v.get(e)?.since??this.#a(),retryAt:n})}async#I(e){this.#m++;try{return await e()}finally{this.#m--}}async check(e){let t=this.#C(e);if(t.disabledReason||this.#e.has(e.id))return null;let{left:n,freesAt:r}=Ro(e,t,this.#a());if(n<=0)return this.#T(e,`hit ${e.maxRunsPerHour} runs this hour; next check at ${new Date(r).toTimeString().slice(0,5)}`,`warn`),this.#t.set(e.id,r),null;let i;try{i=await this.#I(()=>this.#w(e).peek(t.etag))}catch(n){if(n instanceof J&&n.ended)return t.disabledReason=`its sign-in ended (unlinked on your workspace, or unused for 90 days)`,this.#T(e,`stopped: ${t.disabledReason}. Copy a new token from its page on your workspace and run \`${w} add <token>\`.`,`fail`),this.#E(),null;if(n instanceof k&&n.status===401){let r=n.code===`agent_removed`;return t.disabledReason=r?`its owner removed it from Bitfolk`:`token rejected (revoked, or the agent was archived)`,this.#T(e,r?`stopped: ${t.disabledReason}. Run \`${w} rm "${e.name}"\` to forget it.`:`stopped: ${t.disabledReason}. Copy a new token from its page on your workspace and run \`${w} add <token>\` (it keeps your options).`,`fail`),this.#E(),null}let r=this.#O(e);return this.#T(e,`peek failed (${n.message}); retrying in ${Fo(r)}`,`warn`),this.#F(e.id,n,this.#D(e,r)),null}return this.#n.delete(e.id),this.#P(e.id),i?(t.etag=i.etag,t.pollMs=i.next_poll_after_s*1e3,this.#E(),i.tier===`silent`||!i.has_work?(this.#D(e,i.next_poll_after_s*1e3),null):{done:this.#L(e,i)}):(this.#D(e,t.pollMs??Oo),null)}#L(e,t,n){let r={child:null,done:Promise.resolve(null),stopped:!1,sessionId:n?.session.id,name:e.name,tool:n?.session.runtime??e.acp?.agent??e.runtime,title:z(n?n.session.task?.title??n.session.goal?.title??`an objective`:`woke for ${jo(t.reasons)}`),joinedAt:this.#a()},i=this.#V(e,t,r,n);return n?.session.retry?.mode===`fix`&&(i=i.then(i=>!i?.resumed||i.status!==`failed`||i.durationMs>=2e4||r.stopped||this.#l?i:(this.#T(e,`couldn't pick up the earlier conversation; starting fresh on the same branch`,`warn`),this.#V(e,t,r,n,{fresh:!0})))),r.done=(n?i.then(t=>this.#B(e,n,t)):i).finally(()=>{this.#e.delete(e.id),this.#p?.()}),this.#e.set(e.id,r),r.done}#R(e,t,n,r){let i=``,a=null,o=()=>a??=(async()=>{let a=this.#z();if(!a)return;let o=null;try{o=bo(t.dir,e)}catch{}let s={branch:t.branch??null,base_sha:n,head_sha:o?.head??null,checkpoint_sha:o?.checkpoint??null,tool_session_id:r.sessionId??null},c=JSON.stringify(s);c!==i&&await a.checkpoint(e,s)&&(i=c)})().finally(()=>{a=null}),s=setInterval(()=>void o(),this.#s);return o(),{async final(){clearInterval(s),await a,await o()},stop:()=>clearInterval(s)}}#z(){let e=this.config.machine;return!e||this.#d?null:this.#u??=new vt(e.api,()=>ar(e,{now:this.#a}))}async checkMachine(){let e=this.config.machine,t=this.#z();if(!e||!t)return[];let n=this.state.agents[$]??={},r={id:$,name:e.name},i;try{i=await this.#I(()=>t.peek())}catch(e){if(e instanceof J&&e.ended||e instanceof k&&e.status===401)return this.#d=`this machine was unlinked, or its sign-in ended`,this.#T(r,`stopped: ${this.#d}. Run \`${w} login\` to link it again.`,`fail`),[];let t=this.#O(r);this.#T(r,`check-in failed (${e.message}); retrying in ${Fo(t)}`,`warn`);let n=this.#a()+t;return this.#t.set($,n),this.#F($,e,n),[]}this.#n.delete(r.id),this.#P($);for(let e of i.cancel??[])this.#j(e);let a=Math.max(1e3,i.next_poll_after_s*1e3);this.#t.set($,this.#a()+a);let o=()=>{[...this.#e.values()].some(e=>e.sessionId&&!e.stopped)&&this.#t.set($,Math.min(this.#t.get($)??1/0,this.#a()+ko))},s=[],c=i.queued;for(this.#b=null,this.#x=null;c>0&&!this.#l;c--){if(this.#e.size>=this.config.maxConcurrent){this.#b=`slot`;break}let{left:i,freesAt:a}=Ro(e,n,this.#a());if(i<=0){this.#b=`cap`,this.#x=a,this.#f!==a&&(this.#f=a,this.#T(r,`hit ${e.maxRunsPerHour} sessions this hour; the rest wait until ${new Date(a).toTimeString().slice(0,5)}`,`warn`)),this.#t.set($,a);break}let o;try{o=await this.#I(()=>t.claim({runtimes:e.runtimes,timeout_min:e.timeoutMin}))}catch(e){this.#T(r,`couldn't take a session (${e.message})`,`warn`);break}if(!o){c=0;break}this.#y=Math.max(0,this.#y-1),n.runStarts=[...(n.runStarts??[]).filter(e=>e>this.#a()-Co),this.#a()],this.#E();let l;try{l=Bo(e,o)}catch(e){this.#T(r,`can't run a session: ${e.message}`,`fail`),await t.finish(o.session.id,{status:`failed`,summary:null,error:e.message}).catch(()=>{});continue}let u=z(o.session.task?.title??o.session.goal?.title??`an objective`);this.#T(l,`joined for a session: ${L.bold(u)} ${I(`·`)} ${L.tool(B(Y(o.session.runtime)))}`,`start`,si(o.session.runtime));let d=await new _t(l.api,l.token).peek().catch(()=>null),f={has_work:!0,tier:`full`,reasons:[`session_requested`,...(d?.reasons??[]).filter(e=>e!==`setup_actions`)],briefing_goal_id:o.session.goal?.id??null,etag:d?.etag??``,next_poll_after_s:60,rules:d?.rules};s.push({done:this.#L(l,f,o)})}return this.#y=this.#b?Math.max(0,c):0,o(),s}async#B(e,t,n){let r=n?.status===`succeeded`?`succeeded`:n?.status===`interrupted`?`canceled`:`failed`,i=null;if(n){let t=n.work?.startSha??null,r={commits:[],commit_count:0,files_changed:0,insertions:0,deletions:0};if(n.work?.branch&&t)try{e.repo&&so(n.work.dir,e.repo),r=_o(n.work.dir,t,n.work.branch)}catch{}i={base:t?.slice(0,12)??null,...r,duration_ms:n.durationMs}}try{await this.#z()?.finish(t.session.id,{status:r,summary:n?.tally.summary??null,error:n?n.status===`succeeded`?null:n.tally.error??n.status:`the session couldn't start on this machine`,branch:n?.work?.branch??null,result:i,input_tokens:n?n.tally.inputTokens:null,output_tokens:n?n.tally.outputTokens:null,cost_micros:n?.tally.costMicros??null,model:((e.runtime===`acp`?n?.tally.model??e.model:e.model??n?.tally.model)??null)?.slice(0,100)??null}),i?.commit_count&&this.#T(e,`left ${L.bold(`${i.commit_count} commit${i.commit_count===1?``:`s`}`)} on ${L.tool(n.work.branch)} ${I(`(${i.files_changed} files)`)}`,`ok`)}catch(t){this.#T(e,`couldn't report the session's end (${t.message}); Bitfolk times it out`,`warn`)}return this.#T(e,`left the workspace`,`idle`),ho(e),e.repo&&So(e.repo),delete this.state.agents[e.id],this.#E(),n}async#V(e,t,r,a,o={}){let c=this.#C(e),l=this.#w(e),u=Oe[e.runtime],d=e.runtime===`acp`?e.acp:void 0;if(e.runtime===`acp`&&!d)throw Error(`${e.name} has runtime acp but no ACP command`);let f=this.#a(),m;try{m=fo(e)}catch(t){return c.etag=void 0,this.#T(e,`can't prepare its workspace: ${t.message}`,`fail`),this.#D(e,this.#O(e)),null}let h=m.branch?go(m.dir):null,g=a?.session.retry??null,_=!!(g?.mode===`fix`&&m.created&&m.branch&&g.checkpoint_sha&&xo(m.dir,g.checkpoint_sha));_&&this.#T(e,`put back the earlier session's uncommitted work on ${L.tool(String(m.branch))}`,`ok`);let v=null;try{v=await l.runStart({client_run_id:fe(),host:se(),runtime:d?d.agent:e.runtime,reasons:t.reasons,tier:t.tier})}catch(n){if(n instanceof k&&n.status===409)return this.#T(e,`is paused by its owner; not starting`,`idle`),this.#D(e,t.next_poll_after_s*1e3||(wo?Oo:6e5)),null;this.#T(e,`couldn't record the run start (${n.message}); running anyway`,`warn`)}v||await l.status(`working`).catch(()=>{});let ee=o.fresh?null:g?.mode===`fix`?u.resumes!==!1&&g.tool_session_id?g.tool_session_id:null:u.resumes!==!1&&!m.created&&c.sessionId&&c.sessionAt&&f-c.sessionAt<432e5?c.sessionId:null,b=!!m.branch&&!!u.runnerCommits?.(e),x=ao(t,{agentName:e.name,api:e.api,branch:m.branch,runnerCommits:b,resumed:!!ee,session:a?{goal:a.session.goal?.title??null,task:a.session.task?.title??null,note:a.session.note,goalDone:a.session.goal?.done_definition??null,taskSpec:a.session.task?.spec??null,asks:a.session.asks??[],requestedBy:a.session.requested_by??`owner`,requestedByAgent:a.session.requested_by_agent??null,retry:g?{mode:g.mode,branch:g.branch,summary:g.summary,error:g.error,status:g.status,restored:_}:void 0}:void 0}),te=u.launch(e,x,ee);c.runStarts=[...(c.runStarts??[]).filter(e=>e>f-Co),f],this.#E(),this.#T(e,`woke ${I(`for ${jo(t.reasons)}`)} → ${L.tool(B(d?`${Y(d.agent)} over ACP`:e.runtime))}${ee?I(`, resuming`):``}`,`start`);let ne=s(A(),`logs`,e.slug);y(ne,{recursive:!0,mode:448});let re=s(ne,`${new Date(f).toISOString().replace(/[:.]/g,`-`)}.jsonl`),ie=new Set([e.token]),ae=e=>[...ie].reduce((e,t)=>e.split(t).join(`[token]`),e),oe=(e,t)=>p(re,JSON.stringify({t:this.#a(),stream:e,line:ae(t)})+`
|
|
77
77
|
`,{mode:384}),ce=to(),ue=or(e),de=e.oauth?setInterval(()=>{e.oauth.expiresAt-this.#a()>6e5||ar(e,{now:this.#a}).then(()=>{ie.add(e.token),or(e)}).catch(t=>this.#T(e,`couldn't renew its sign-in mid-run: ${t.message}`,`warn`))},6e4):null,C=ge();r.tally=C,r.startedAt=this.#a();let pe={...Vo(),PATH:[ce,...Ho(),...lr()].join(i),...d?{NO_BROWSER:`1`}:{BITFOLK_TOKEN:e.token},BITFOLK_TOKEN_FILE:ue,BITFOLK_API:e.api.replace(/\/+$/,``),BITFOLK_AGENT:e.name,...v?{BITFOLK_RUN_ID:v.id}:{}};d&&delete pe.BITFOLK_TOKEN;for(let[e,t]of Object.entries(pe))t&&t.length>=12&&/KEY|TOKEN|SECRET|PASSWORD|PASSWD|CREDENTIAL|AUTH/i.test(e)&&e!==`BITFOLK_TOKEN_FILE`&&ie.add(t);let w=a?this.#M(a.session.id,C):null,me=a&&m.branch?this.#R(a.session.id,m,h,C):null,he=null,T=t=>{let n=t&&z(ae(t)).trim();w?.add(n),n&&n!==he&&(he=n,r.activity=n,r.activityAt=this.#a(),this.#T(e,n,`progress`))},_e=null,ve=null,E=!1,ye,be=null;try{ye=await new Promise(t=>{let i=n(te.bin,te.args,{cwd:m.dir,env:pe,stdio:[`pipe`,`pipe`,`pipe`],detached:!0});if(r.child=i,i.stdin.on(`error`,()=>{}),d){let t=new mt({stdin:i.stdin,stdout:i.stdout,agent:{...e,acp:d},label:Y(d.agent),cwd:m.dir,home:A(),packet:x,tally:C,clientVersion:O,log:oe,activity:T,kill:()=>{No.has(i)||Po(i)},cancelGraceMs:this.#c});r.cancel=e=>t.cancel(e),be=t.run()}else i.stdin.end(te.stdin),le({input:i.stdout}).on(`line`,e=>{oe(`out`,e),u.parse(e,C),u.activity&&T(u.activity(e,m.dir))});(this.#l||r.stopped)&&this.#k(r,`owner`),le({input:i.stderr}).on(`line`,e=>{oe(`err`,e),e.trim()&&(ve=ae(e.trim()).slice(0,300))});let a=setTimeout(()=>{E=!0,this.#k(r,`timeout`)||(E=!1)},e.timeoutMin*6e4);i.on(`error`,e=>{_e=e.code===`ENOENT`?`${te.bin} isn't installed or isn't on PATH`:e.message}),i.on(`close`,e=>{No.add(i),clearTimeout(a),t({code:e})})})}finally{w?.stop(),me?.stop(),de&&clearInterval(de)}let xe=be?await be:null;r.cancel=void 0,u.afterExit?.(te,C),S(ue,{force:!0});let Se=this.#a()-f,D,Ce;if(xe?(D=r.stopped?`interrupted`:C.overBudget?`over_budget`:_e||E||!xe.ok?`failed`:`succeeded`,Ce=_e??(D===`over_budget`?C.error:E&&D===`failed`?`stopped after ${e.timeoutMin} minutes`:D===`failed`?xe.exited?ve??`${xe.error??`the tool exited`}${ye.code===null?``:` (exit ${ye.code})`}`:xe.error??ve??`the turn failed`:null),D===`failed`&&(C.error=Ce)):(D=r.stopped?`interrupted`:C.overBudget?`over_budget`:_e||E||ye.code!==0||C.isError?`failed`:`succeeded`,Ce=_e??(E?`stopped after ${e.timeoutMin} minutes`:D===`failed`?C.error??ve??`exit ${ye.code}`:null)),D===`failed`&&!C.error&&(C.error=Ce),b&&!_e){let t=`${e.name}: ${C.summary??`work from a Bitfolk run`}`,n=`Bitfolk run${v?` ${v.id}`:``}, ${D}.`;try{e.repo&&m.branch&&so(m.dir,e.repo);let r=po(m.dir,`${t.length>72?t.slice(0,71)+`…`:t}\n\n${n}`);r&&this.#T(e,`committed its changes as ${L.bold(r)} on ${L.tool(String(m.branch))}`,`ok`)}catch(t){this.#T(e,`couldn't commit its changes: ${t.message.split(`
|
|
78
|
-
`)[0]}`,`warn`)}}await me?.final().catch(()=>{}),D===`failed`&&ee&&Se<2e4?(c.sessionId=void 0,c.etag=void 0):C.sessionId&&(c.sessionId=C.sessionId,c.sessionAt=this.#a()),(D===`failed`||D===`interrupted`)&&(c.etag=void 0),this.#E();let we={status:D,exit_code:xe?xe.ok&&D===`succeeded`?0:null:ye.code,session_id:C.sessionId,input_tokens:C.inputTokens,output_tokens:C.outputTokens,cost_micros:C.costMicros,summary:C.summary,error:Ce};v?await l.runFinish(v.id,we).catch(async()=>{await new Promise(e=>setTimeout(e,2e3)),await l.runFinish(v.id,we).catch(t=>this.#T(e,`couldn't record the run end: ${t.message}`,`warn`))}):await l.status(`idle`).catch(()=>{});let Te=this.#N();D===`succeeded`?Te.done++:D===`interrupted`?Te.stopped++:Te.failed++,Te.costMicros+=C.costMicros??0,Te.durationMs+=Se;let Ee=Io(C),De=D===`succeeded`?L.ok(L.bold(D)):D===`failed`?L.err(L.bold(D)):L.warn(L.bold(D));return this.#T(e,[`${De} in ${Fo(Se)}`,...Ee?Ee.split(`, `).map(e=>e.startsWith(`$`)?L.money(e):e):[]].join(` ${I(`·`)} `)+`${Ce?` ${L.err(`(${z(Ce)})`)}`:``}${C.summary?` ${I(`—`)} ${z(C.summary)}`:``}`,D===`succeeded`?`ok`:D===`failed`?`fail`:`warn`),D===`failed`?this.#D(e,this.#O(e)):(this.#n.delete(e.id),this.#D(e,To)),{agent:e.name,status:D,tally:C,durationMs:Se,work:{dir:m.dir,branch:m.branch,startSha:h},resumed:!!ee}}async run(e={}){this.#h=!!e.once;let t=this.config.agents.filter(e=>!this.#C(e).disabledReason);for(let e of this.config.agents){let t=this.#C(e).disabledReason;t&&this.#T(e,`skipped: ${t}`,`idle`)}let n=this.config.machine;if(t.length===0&&!n)return this.#T(null,`nothing to run. Link this computer with \`${w} login\`.`,`warn`),[];if(e.once){let e=[];if(n){let t=await this.checkMachine();t.length===0&&!this.#d&&this.#T({name:n.name},`no sessions waiting`,`idle`),e.push(...t.map(e=>e.done))}for(let n of t){for(;this.#e.size>=this.config.maxConcurrent;)await Promise.race([...this.#e.values()].map(e=>e.done));let t=await this.check(n);t?e.push(t.done):this.#e.has(n.id)||this.#T(n,`nothing to do`,`idle`)}let r=n?setInterval(()=>void this.#A(),ko):null;try{return(await Promise.all(e)).filter(e=>e!==null)}finally{r&&clearInterval(r)}}let r=[],i=[n?`${n.name} for sessions`:null,t.length?`${t.length} agent${t.length===1?``:`s`}`:null].filter(Boolean).join(` and `);for(this.#T(null,`watching ${L.bold(i)} ${I(`(up to ${this.config.maxConcurrent} at once).${this.#g?``:` Ctrl-C to stop.`}`)}`,`start`);!this.#l;){let e=this.#a();if(n&&!this.#d&&(this.#t.get($)??0)<=e)for(let e of await this.checkMachine())e.done.then(e=>e&&r.push(e));for(let n of t){if(this.#l||this.#e.size>=this.config.maxConcurrent)break;this.#e.has(n.id)||this.#C(n).disabledReason||(this.#t.get(n.id)??0)>e||(await this.check(n))?.done.then(e=>e&&r.push(e))}let i=Math.min(...t.map(t=>this.#t.get(t.id)??e),n&&!this.#d?this.#t.get($)??e:1/0),a=Math.max(1e3,Math.min(6e4,i-this.#a()));await new Promise(e=>{let t=setTimeout(e,a);this.#p=()=>{clearTimeout(t),e()}}),this.#p=null}return await Promise.all([...this.#e.values()].map(e=>e.done)),r}};const Wo=e=>Atomics.wait(new Int32Array(new SharedArrayBuffer(4)),0,0,e);function Go(e){for(let t=0;t<20;t++){let t;try{t=x(e,`utf8`).trim()}catch{return null}if(t)return Number(t)||0;Wo(25)}return 0}function Ko(e){if(!e||e===process.pid)return!1;try{return process.kill(e,0),!0}catch(e){return e.code===`EPERM`}}function qo(){let e=Go(s(A(),`runner.pid`));return e&&Ko(e)?e:null}function Jo(){let e=s(A(),`runner.pid`);y(A(),{recursive:!0,mode:448});let t=String(process.pid),n=()=>{try{x(e,`utf8`).trim()===t&&S(e,{force:!0})}catch{}};for(let r=0;r<3;r++){try{return ae(e,t,{mode:384,flag:`wx`}),n}catch(e){if(e.code!==`EEXIST`)throw e}let r=Go(e);if(r===null)continue;if(Ko(r))throw Error(`a runner is already going (pid ${r}). Stop it first, or see \`${w} status\`.`);let i=`${e}.${process.pid}.tmp`;if(ae(i,t,{mode:384}),re(i,e),Wo(25),Go(e)===process.pid)return n}throw Error(`another runner is starting at the same moment. Try again, or see \`${w} status\`.`)}function Yo(e){let t;try{t=JSON.parse(e)}catch{return null}if(t.msg===`started tunnel`)return{kind:`started`};let n=typeof t.err==`string`&&t.err!==`<nil>`?t.err:null;return t.lvl===`crit`||t.lvl===`eror`&&n?{kind:`error`,message:
|
|
78
|
+
`)[0]}`,`warn`)}}await me?.final().catch(()=>{}),D===`failed`&&ee&&Se<2e4?(c.sessionId=void 0,c.etag=void 0):C.sessionId&&(c.sessionId=C.sessionId,c.sessionAt=this.#a()),(D===`failed`||D===`interrupted`)&&(c.etag=void 0),this.#E();let we={status:D,exit_code:xe?xe.ok&&D===`succeeded`?0:null:ye.code,session_id:C.sessionId,input_tokens:C.inputTokens,output_tokens:C.outputTokens,cost_micros:C.costMicros,summary:C.summary,error:Ce};v?await l.runFinish(v.id,we).catch(async()=>{await new Promise(e=>setTimeout(e,2e3)),await l.runFinish(v.id,we).catch(t=>this.#T(e,`couldn't record the run end: ${t.message}`,`warn`))}):await l.status(`idle`).catch(()=>{});let Te=this.#N();D===`succeeded`?Te.done++:D===`interrupted`?Te.stopped++:Te.failed++,Te.costMicros+=C.costMicros??0,Te.durationMs+=Se;let Ee=Io(C),De=D===`succeeded`?L.ok(L.bold(D)):D===`failed`?L.err(L.bold(D)):L.warn(L.bold(D));return this.#T(e,[`${De} in ${Fo(Se)}`,...Ee?Ee.split(`, `).map(e=>e.startsWith(`$`)?L.money(e):e):[]].join(` ${I(`·`)} `)+`${Ce?` ${L.err(`(${z(Ce)})`)}`:``}${C.summary?` ${I(`—`)} ${z(C.summary)}`:``}`,D===`succeeded`?`ok`:D===`failed`?`fail`:`warn`),D===`failed`?this.#D(e,this.#O(e)):(this.#n.delete(e.id),this.#D(e,To)),{agent:e.name,status:D,tally:C,durationMs:Se,work:{dir:m.dir,branch:m.branch,startSha:h},resumed:!!ee}}async run(e={}){this.#h=!!e.once;let t=this.config.agents.filter(e=>!this.#C(e).disabledReason);for(let e of this.config.agents){let t=this.#C(e).disabledReason;t&&this.#T(e,`skipped: ${t}`,`idle`)}let n=this.config.machine;if(t.length===0&&!n)return this.#T(null,`nothing to run. Link this computer with \`${w} login\`.`,`warn`),[];if(e.once){let e=[];if(n){let t=await this.checkMachine();t.length===0&&!this.#d&&this.#T({name:n.name},`no sessions waiting`,`idle`),e.push(...t.map(e=>e.done))}for(let n of t){for(;this.#e.size>=this.config.maxConcurrent;)await Promise.race([...this.#e.values()].map(e=>e.done));let t=await this.check(n);t?e.push(t.done):this.#e.has(n.id)||this.#T(n,`nothing to do`,`idle`)}let r=n?setInterval(()=>void this.#A(),ko):null;try{return(await Promise.all(e)).filter(e=>e!==null)}finally{r&&clearInterval(r)}}let r=[],i=[n?`${n.name} for sessions`:null,t.length?`${t.length} agent${t.length===1?``:`s`}`:null].filter(Boolean).join(` and `);for(this.#T(null,`watching ${L.bold(i)} ${I(`(up to ${this.config.maxConcurrent} at once).${this.#g?``:` Ctrl-C to stop.`}`)}`,`start`);!this.#l;){let e=this.#a();if(n&&!this.#d&&(this.#t.get($)??0)<=e)for(let e of await this.checkMachine())e.done.then(e=>e&&r.push(e));for(let n of t){if(this.#l||this.#e.size>=this.config.maxConcurrent)break;this.#e.has(n.id)||this.#C(n).disabledReason||(this.#t.get(n.id)??0)>e||(await this.check(n))?.done.then(e=>e&&r.push(e))}let i=Math.min(...t.map(t=>this.#t.get(t.id)??e),n&&!this.#d?this.#t.get($)??e:1/0),a=Math.max(1e3,Math.min(6e4,i-this.#a()));await new Promise(e=>{let t=setTimeout(e,a);this.#p=()=>{clearTimeout(t),e()}}),this.#p=null}return await Promise.all([...this.#e.values()].map(e=>e.done)),r}};const Wo=e=>Atomics.wait(new Int32Array(new SharedArrayBuffer(4)),0,0,e);function Go(e){for(let t=0;t<20;t++){let t;try{t=x(e,`utf8`).trim()}catch{return null}if(t)return Number(t)||0;Wo(25)}return 0}function Ko(e){if(!e||e===process.pid)return!1;try{return process.kill(e,0),!0}catch(e){return e.code===`EPERM`}}function qo(){let e=Go(s(A(),`runner.pid`));return e&&Ko(e)?e:null}function Jo(){let e=s(A(),`runner.pid`);y(A(),{recursive:!0,mode:448});let t=String(process.pid),n=()=>{try{x(e,`utf8`).trim()===t&&S(e,{force:!0})}catch{}};for(let r=0;r<3;r++){try{return ae(e,t,{mode:384,flag:`wx`}),n}catch(e){if(e.code!==`EEXIST`)throw e}let r=Go(e);if(r===null)continue;if(Ko(r))throw Error(`a runner is already going (pid ${r}). Stop it first, or see \`${w} status\`.`);let i=`${e}.${process.pid}.tmp`;if(ae(i,t,{mode:384}),re(i,e),Wo(25),Go(e)===process.pid)return n}throw Error(`another runner is starting at the same moment. Try again, or see \`${w} status\`.`)}function Yo(e,t,n){return e===`public`?L.warn(`anyone with the link, no sign-in`):e===`personal`?`only you, Google sign-in with your email`:`${t} only, Google sign-in${n===void 0?``:` (${n} email${n===1?``:`s`} let in)`}`}const Xo=e=>e===`personal`?`your Settings, Shared apps (only you see it)`:`your workspace, Settings, Shared apps`;function Zo(e){let t;try{t=JSON.parse(e)}catch{return null}if(t.msg===`started tunnel`)return{kind:`started`};let n=typeof t.err==`string`&&t.err!==`<nil>`?t.err:null;return t.lvl===`crit`||t.lvl===`eror`&&n?{kind:`error`,message:Qo(String(n??t.msg??``))}:null}function Qo(e){return e.replace(/\s+/g,` `).replace(/\bERR_NGROK_\d+\b.*$/,``).trim().slice(0,240)||`ngrok failed`}async function $o(){try{let e=await import(`@ngrok/ngrok`),t=e.default??e;return typeof t.forward==`function`?t:null}catch{return null}}async function es(e,t=1e4,n=fetch){let r=Date.now()+t;for(;Date.now()<r;){try{if((await n(e,{method:`HEAD`,redirect:`manual`,signal:AbortSignal.timeout(3e3)})).status!==404)return!0}catch{}await new Promise(e=>setTimeout(e,500))}return!1}async function ts(e,t){let r=await $o();if(r){let n=!1,i,a=await Promise.race([r.forward({addr:`127.0.0.1:${e.port}`,authtoken:e.authtoken,domain:new URL(e.internalUrl).host,binding:`internal`,onStatusChange:e=>{n&&(e===`closed`&&t.lost(),e===`connected`&&t.back())}}),new Promise((e,t)=>i=setTimeout(()=>t(Error(`the tunnel took too long to open`)),3e4))]).catch(e=>{throw Error(Qo(e instanceof Error?e.message:String(e)))}).finally(()=>clearTimeout(i));return n=!0,{closed:new Promise(()=>{}),close:()=>void a.close().catch(()=>null)}}let i=dr(`ngrok`);if(!i)throw Error(`the tunnel library that comes with bitfolk isn't installed for this computer. Reinstall with npm i -g bitfolk (without --omit=optional), or install the ngrok program (https://ngrok.com/download). No ngrok account is needed.`);ae(e.cfgFile,`version: "3"
|
|
79
79
|
agent:
|
|
80
80
|
web_addr: false
|
|
81
81
|
update_check: false
|
|
82
82
|
`,{mode:384});let a=n(i,[`http`,String(e.port),`--url`,e.internalUrl,`--config`,e.cfgFile,`--log`,`stdout`,`--log-format`,`json`],{env:{...process.env,NGROK_AUTHTOKEN:e.authtoken,NGROK_API_KEY:``,NGROK_CONFIG:``},stdio:[`ignore`,`pipe`,`pipe`]}),o=()=>{a.kill(`SIGTERM`),S(e.cfgFile,{force:!0})},s=await new Promise(e=>{let t=``,n=n=>{t+=n.toString();let r=t.split(`
|
|
83
|
-
`);t=r.pop()??``;for(let t of r){let n=
|
|
84
|
-
`))}async function
|
|
85
|
-
`));let te=0;h=setInterval(async()=>{try{let e=await l.beatApp(d.app_id);te=0,e.status===`stopped`&&await v(0,`Stopped sharing ${d.name}. ${
|
|
86
|
-
`),
|
|
87
|
-
`)))}async function
|
|
88
|
-
`);function
|
|
83
|
+
`);t=r.pop()??``;for(let t of r){let n=Zo(t);n?.kind===`started`&&e(null),n?.kind===`error`&&e(n.message)}};a.stdout.on(`data`,n),a.stderr.on(`data`,n),a.once(`exit`,()=>e(`ngrok stopped before the tunnel opened`)),setTimeout(()=>e(`ngrok took too long to open the tunnel`),3e4)});if(s)throw o(),Error(s);return{closed:new Promise(e=>a.once(`exit`,()=>e(`ngrok stopped`))),close:o}}function ns(e,t=`127.0.0.1`){return new Promise(n=>{let r=pe({port:e,host:t}),i=e=>{r.destroy(),n(e)};r.setTimeout(1500,()=>i(!1)),r.once(`connect`,()=>i(!0)),r.once(`error`,()=>i(!1))})}const rs={stopped_in_town:`Someone stopped it from the workspace's Settings.`,replaced:`It was shared again from this computer.`,machine_unlinked:`This computer was unlinked from the workspace.`,went_quiet:`This computer stopped checking in for a few minutes.`},is=`BITFOLK_SHARE_READY`,as=()=>s(A(),`share`),os=e=>s(as(),`port-${e}.json`),ss=(e=process.env)=>!!(e.BITFOLK_RUN_ID||e.BITFOLK_TOKEN_FILE);async function cs(e,t=process.env,n=fetch){let r=`A link anyone can open needs your owner's yes first: bitfolk-api POST /v1/actions with action_type post_public, a title, why, and target "<the app> at a public link". Once it says allowed, run this again with --approval <request id>. A members-only link (no --public) needs no approval.`;if(!e)return r;if(!/^[0-9a-f-]{36}$/i.test(e))return`--approval takes the request id (a uuid) from POST /v1/actions.`;let i=t.BITFOLK_TOKEN??``;try{t.BITFOLK_TOKEN_FILE&&(i=x(t.BITFOLK_TOKEN_FILE,`utf8`).trim())}catch{}let a=(t.BITFOLK_API??``).replace(/\/+$/,``);if(!i||!a)return`can't check the approval: this session has no Bitfolk token. `+r;let o;try{let t=await n(`${a}/v1/actions/${e}`,{headers:{authorization:`Bearer ${i}`,"user-agent":`bitfolk-share`}});if(!t.ok)return`can't read approval ${e} (HTTP ${t.status}). Use the id of a request you sent.`;o=await t.json()}catch(t){return`can't reach Bitfolk to check approval ${e}: ${t.message}`}return o.action_type===`post_public`?o.status===`allowed`?null:`approval ${e} is ${o.status??`not decided`}, not allowed. Wait for it, or share without --public.`:`approval ${e} is for ${o.action_type??`something else`}, not post_public. ${r}`}function ls(e,t){if(e)try{process.kill(-e,t)}catch{try{process.kill(e,t)}catch{}}}async function us(e,t,n){let r;t?.once(`exit`,e=>r=e);let i=Date.now()+n;for(;Date.now()<i;){if(await ns(e))return null;if(r!==void 0)return`the app stopped (exit ${r}) before anything listened on 127.0.0.1:${e}`;await new Promise(e=>setTimeout(e,500))}return`nothing listened on 127.0.0.1:${e} within ${Math.round(n/1e3)}s`}async function ds(e){let t;try{t=JSON.parse(x(os(e),`utf8`))}catch{throw Error(`nothing here is shared in the background on port ${e}. A share in a terminal stops with Ctrl-C there.`)}try{process.kill(t.pid,`SIGTERM`)}catch{S(os(e),{force:!0}),console.log(`${I(R.idle)} ${F(t.name)} had already stopped sharing.`);return}for(let t=0;t<20&&_(os(e));t++)await new Promise(e=>setTimeout(e,250));console.log(`${I(R.idle)} Stopped sharing ${F(t.name)}. The link no longer works.`)}async function fs(e,t){y(as(),{recursive:!0,mode:448});let r=s(as(),`ready-${e}-${process.pid}.json`),i=s(as(),`port-${e}.log`);S(r,{force:!0});let a=b(i,`w`,384),o=n(process.execPath,[...process.execArgv,process.argv[1],...process.argv.slice(2),`--no-color`],{cwd:process.cwd(),env:{...process.env,[is]:r},detached:!0,stdio:[`ignore`,a,a]});o.unref();let c=Vn(`Opening a link for 127.0.0.1:${e}`,{quiet:!0}),l=Date.now()+(t.run?18e4:6e4),u=null;for(;!u&&Date.now()<l;){await new Promise(e=>setTimeout(e,400));try{u=JSON.parse(x(r,`utf8`))}catch{o.exitCode!==null&&(u={ok:!1,error:`sharing stopped before the link opened. Log: ${i}`})}}if(c.stop(),S(r,{force:!0}),!u)throw ls(o.pid,`SIGTERM`),Error(`the link didn't open in time. Log: ${i}`);if(!u.ok)throw Error(u.error);let d=Yo(u.access,u.town);console.log([G(`${F(u.name)} is shared for up to ${t.keep} hour${t.keep===1?``:`s`}`),...xn([Cn(u.url),``,...bn([[`who`,d],[`from`,`127.0.0.1:${e}${t.run?`, started with ${t.run}`:``}`],[`listed`,Xo(u.access)],[`log`,i]])]),I(` Running in the background. Stop it with: bitfolk share stop ${e}`)].join(`
|
|
84
|
+
`))}async function ps(e,t,r){if(!await $o()&&!dr(`ngrok`))throw Error(`the tunnel library that comes with bitfolk isn't installed for this computer. Reinstall with npm i -g bitfolk (without --omit=optional), or install the ngrok program (https://ngrok.com/download). No ngrok account is needed.`);let i=process.env[is],a=e=>i&&ae(i,JSON.stringify(e),{mode:384}),o=e=>{throw a({ok:!1,error:e}),Error(e)};if(r.public&&ss()){let e=await cs(r.approval);e&&o(e)}if(r.keep&&!i)return fs(t,r);!r.run&&!await ns(t)&&o(`nothing is running on 127.0.0.1:${t}. Start your app first, or pass --run "<command>".`),r.run&&await ns(t)&&o(`something already listens on 127.0.0.1:${t}. Pick a free port for --run, or share that app without --run.`);let c=null;if(r.run){y(as(),{recursive:!0,mode:448});let e=b(s(as(),`port-${t}.app.log`),`w`,384);c=n(`sh`,[`-c`,r.run],{cwd:process.cwd(),env:{...process.env,PORT:String(t)},detached:!0,stdio:[`ignore`,e,e]});let i=Vn(`Starting ${r.run}`,{quiet:!0}),a=await us(t,c,15e4);i.stop(),a&&(ls(c.pid,`SIGTERM`),o(`${a}. App log: ${s(as(),`port-${t}.app.log`)}`))}let l=new vt(e.api,()=>ar(e)),u=Vn(`Opening a link for 127.0.0.1:${t}`,{quiet:!0}),d;try{d=await l.openApp({name:r.name??``,port:t,access:r.public?`public`:r.personal?`personal`:`town`})}catch(e){return ls(c?.pid,`SIGTERM`),o(e.message)}finally{u.stop()}let f=as();y(f,{recursive:!0,mode:448});let p=s(f,`${d.app_id}.yml`),m=null,h=null,g=!1,_=null,v=async(e,n)=>{if(!g){if(g=!0,_?.stop(),h&&clearInterval(h),m?.close(),ls(c?.pid,`SIGTERM`),S(p,{force:!0}),i)try{JSON.parse(x(os(t),`utf8`)).pid===process.pid&&S(os(t),{force:!0})}catch{}await l.stopApp(d.app_id).catch(()=>null),console.log(n),process.exit(e)}};process.on(`SIGINT`,()=>v(0,`\n${I(R.idle)} Stopped sharing ${F(d.name)}. The link no longer works.`)),process.on(`SIGTERM`,()=>v(0,`${I(R.idle)} Stopped sharing ${F(d.name)}.`)),process.on(`SIGHUP`,()=>v(0,`${I(R.idle)} Stopped sharing ${F(d.name)}.`)),_=Vn(`Connecting the tunnel`,{quiet:!0,hint:`Ctrl-C to cancel`});try{m=await ts({port:t,internalUrl:d.internal_url,authtoken:d.authtoken,cfgFile:p},{lost:()=>console.log(wn(`Lost the tunnel connection; reconnecting.`)),back:()=>console.log(G(`Tunnel reconnected.`))})}catch(e){_.stop();let t=e.message;return a({ok:!1,error:`couldn't share ${d.name}: ${t}`}),v(1,Tn(`couldn't share ${d.name}: ${t}`,`bitfolk: `))}if(await es(d.url),_.stop(),m.closed.then(e=>v(1,Tn(`${e}, so ${d.name} is no longer shared.`))),c?.once(`exit`,e=>v(1,Tn(`the app stopped (exit ${e}), so ${d.name} is no longer shared.`))),i){let e=r.keep?new Date(Date.now()+r.keep*36e5).toISOString():null,n={pid:process.pid,port:t,app_id:d.app_id,name:d.name,url:d.url,until:e};ae(os(t),JSON.stringify(n),{mode:384}),r.keep&&setTimeout(()=>v(0,`Shared ${d.name} for ${r.keep} hours; stopped.`),r.keep*36e5).unref(),a({ok:!0,url:d.url,name:d.name,access:d.access,town:d.town})}let ee=Yo(d.access,d.town,d.members);console.log([G(`${F(d.name)} is shared`),...xn([Cn(d.url),``,...bn([[`who`,ee],[`from`,`127.0.0.1:${t}`],[`listed`,Xo(d.access)]])]),I(i?` In the background. Stop it with: bitfolk share stop ${t}`:` Keep this running to stay shared. Ctrl-C stops it.`)].join(`
|
|
85
|
+
`));let te=0;h=setInterval(async()=>{try{let e=await l.beatApp(d.app_id);te=0,e.status===`stopped`&&await v(0,`Stopped sharing ${d.name}. ${rs[e.stop_reason??``]??``}`.trim())}catch{++te===2&&console.log(wn(`Can't reach your workspace right now; the link keeps working for a few minutes while this retries.`))}},Math.max(15,d.beat_s)*1e3)}const ms={repo:{type:`string`},base:{type:`string`},runtime:{type:`string`},command:{type:`string`},acp:{type:`string`},model:{type:`string`},permission:{type:`string`},"max-usd":{type:`string`},"max-runs":{type:`string`},timeout:{type:`string`},api:{type:`string`},email:{type:`string`},name:{type:`string`},public:{type:`boolean`},personal:{type:`boolean`},run:{type:`string`},keep:{type:`string`},approval:{type:`string`},once:{type:`boolean`},goal:{type:`string`},task:{type:`string`},detach:{type:`boolean`},follow:{type:`boolean`,short:`f`},json:{type:`boolean`},all:{type:`boolean`},limit:{type:`string`},add:{type:`string`},spec:{type:`string`},note:{type:`string`},"let-sleep":{type:`boolean`},"no-browser":{type:`boolean`},"no-color":{type:`boolean`},agent:{type:`boolean`},tool:{type:`boolean`},force:{type:`boolean`},help:{type:`boolean`,short:`h`},version:{type:`boolean`,short:`v`}};function hs(e,t,n,r){if(e===void 0)return;let i=Number(e);if(!Number.isFinite(i)||i<n||i>r)throw new X(`--${t} must be a number from ${n} to ${r}`);return i}function gs(e){if(!e)return!1;try{return t(`sh`,[`-c`,`command -v "$1"`,`sh`,e],{stdio:`ignore`}),!0}catch{return!1}}function _s(e,t){let n={...e};if(t.runtime!==void 0){if(!yt.includes(t.runtime))throw new X(`--runtime must be one of ${yt.join(`, `)}`);n.runtime=t.runtime}if(t.command!==void 0&&(n.command=t.command),n.runtime===`command`&&!n.command)throw new X(`runtime command needs --command "..."`);if(t.permission!==void 0){if(!bt.includes(t.permission))throw new X(`--permission must be one of ${bt.join(`, `)}`);n.permission=t.permission}if(t.repo!==void 0){let e=co(l(t.repo));if(!e)throw new X(`${t.repo} isn't inside a git repository`);n.repo=e}return t.base!==void 0&&(n.base=t.base),t.model!==void 0&&(n.model=t.model||void 0),n.maxUsdPerRun=hs(t[`max-usd`],`max-usd`,.01,100)??n.maxUsdPerRun,n.maxRunsPerHour=hs(t[`max-runs`],`max-runs`,1,60)??n.maxRunsPerHour,n.timeoutMin=hs(t.timeout,`timeout`,1,600)??n.timeoutMin,n}function vs(e){return Si(e,e.repo?`${e.repo} ${I(`(branch ${lo(e)})`)}`:I(`no repo`),Ot().agents[e.id]?.disabledReason)}const ys=e=>Ci([vs(e)])[0].join(`
|
|
86
|
+
`),bs=e=>(e.api??process.env.BITFOLK_API??`https://bitfolk.ai`).replace(/\/+$/,``);async function xs(e,t){if(e??=process.env.BITFOLK_TOKEN,!e||!/^bfa_[A-Za-z0-9]{20,}$/.test(e))throw new X(`give the agent's token from its page on your workspace: ${w} add bfa_... (to link this computer instead, run ${w} login)`);let n=bs(t),r=await q(`Asking your workspace about this agent`,()=>new _t(n,e).setup());await Ns(n,e,r,t,void 0)}const Ss=(e=[])=>[...hr().filter(e=>e.supported).map(e=>e.id),...e.map(e=>gr(e.name))];function Cs(){let e=hr().filter(e=>!e.supported);e.length!==0&&(console.log(`\n ${F(`Also found`)} ${I(`(not built in yet; add one with ${w} add, then check the command works headless):`)}`),console.log(vn(e.map(e=>[e.label,W(`${w} add ${e.id.replace(/_/g,`-`)} --command '${e.suggest}'`)]),{indent:4}).join(`
|
|
87
|
+
`)))}async function ws(e){let t=bs(e),n=j().machine,{accessToken:r,link:i,me:a}=await rr(t,{browser:!e[`no-browser`],detected:Ss(n?.customTools)});n&&n.id!==a.machine.id&&await sr(n);let o=Ts({...xt,...n??{},id:a.machine.id,name:a.machine.name,api:t,token:r,oauth:i,runtimes:a.machine.runtimes,customTools:n?.customTools,workspace:a.workspace?.name},e),s=j();s.machine=o,Dt(s),console.log(``),await oi(Ei(o,a.workspace?.name??`your workspace`)),console.log(``);for(let e of o.runtimes){let t=cr.find(t=>t.id===e);t&&!fr(t)&&console.log(` ${wn(`warning: ${t.label} isn't installed here yet; install it and sign in before sessions start.`)}`)}Cs(),o.repo||console.log(` ${I(`no repo yet: builders work in plain folders. Give them one with:`)} ${W(`${w} set --repo <path>`)}`),console.log(`\nKeep it running with: ${F(W(`${w} up`))}`),me&&console.log(I(`To keep the command around, install it once: ${W(`npm i -g bitfolk`)} (then use bitfolk up).`))}function Ts(e,t){let n=_s({...e,slug:`machine`,runtime:`claude_code`},{...t,runtime:void 0,command:void 0});return{...e,repo:n.repo,base:n.base,model:n.model,permission:n.permission,maxUsdPerRun:n.maxUsdPerRun,maxRunsPerHour:n.maxRunsPerHour,timeoutMin:n.timeoutMin}}const Es=e=>Ci([xi(e)])[0].join(`
|
|
88
|
+
`);function Ds(){let e=hr({versions:!0}),t=j().machine;if(e.length===0&&!t?.customTools?.length){console.log(Tn(`No coding tools found. Install Claude Code, Codex, OpenCode or Hermes, then run ${w} login.`));return}console.log(F(L.accent(`Coding tools on this computer`)));let n=e=>`${L.ok(R.dot)} ${L.ok(e)}`,r=e=>`${I(R.idle)} ${I(e)}`,i=[],a=P()<60,o=e=>a||!si(e)?``:si(e)+` `;for(let s of e){let e=s.acp?` via ACP`:``,c=s.supported?t?t.runtimes.includes(s.id)?n(`on${e}`):`${r(`off${e}`)} ${I(`(`)}${W(`${w} add ${s.id.replace(/_/g,`-`)}`)}${I(`)`)}`:n(`supported${e}`):`${L.warn(R.skip)} ${L.warn(`not built in`)} ${I(`(${w} add ${s.id.replace(/_/g,`-`)} --command ...)`)}`,l=I(H(Os(s.version,s.label),16));a?i.push([L.tool(F(s.label))],[` ${l}${s.version?I(` · `):``}${c}`],[` ${I(s.path)}`]):(i.push([o(s.id)+L.tool(F(s.label)),l,c]),i.push([``,I(s.path)]))}for(let e of t?.customTools??[]){let s=e.protocol===`acp`?`custom, via ACP`:`custom`;i.push([(a?``:o(gr(e.name)))+L.tool(F(e.name)),I(s),`${t.runtimes.includes(gr(e.name))?n(`on`):r(`off`)}${I(`:`)} ${e.command}`])}console.log(vn(i,{indent:2,gap:2}).join(`
|
|
89
89
|
`)),(e.some(e=>e.supported&&e.acp)||t?.customTools?.some(e=>e.protocol===`acp`))&&console.log(I(`
|
|
90
90
|
Via ACP: sign in to each tool in a terminal first. Safe mode is enforced through the tool's permission requests
|
|
91
91
|
(its own settings may allow more), and --max-usd stops a session once its reported cost passes the cap.`)),t||console.log(`
|
|
92
|
-
`+K(`Link this computer with ${W(`${w} login`)} to let your workspace start them.`,0))}function
|
|
93
|
-
`)),console.log(` ${
|
|
92
|
+
`+K(`Link this computer with ${W(`${w} login`)} to let your workspace start them.`,0))}function Os(e,t){let n=(e??``).replace(/\(.*$/,``).replace(new RegExp(t.replace(/[.*+?^${}()|[\]\\]/g,`\\$&`),`ig`),``).trim().split(/\s+/),r=n.findIndex(e=>/\d/.test(e));return(r>=0?n.slice(r):n).join(` `).trim()}const ks=(e,t)=>q(`Telling your workspace`,()=>new vt(e.api,()=>ar(e)).setTools(t));async function As(e,t,n){let r=j().machine;if(!r)throw new X(`link this computer first: ${w} login`);if(!t||!vr.test(t))throw new X(`give the tool a short name: lowercase letters, numbers and dashes. See ${w} tools`);if(n.command!==void 0&&n.acp!==void 0)throw new X(`give --command or --acp, not both`);let i=_r(e,t,r,n);if(i){e===`add`&&!fr(i)&&console.log(wn(`warning: ${i.label} isn't installed here yet.`));let t=e===`add`?[...new Set([...r.runtimes,i.id])]:r.runtimes.filter(e=>e!==i.id);if(t.length===0)throw new X(`that's the last tool on this computer; keep at least one, or run ${w} logout`);let n=await ks(r,t),a=j();return a.machine={...a.machine,runtimes:n.runtimes},Dt(a),console.log(G(e===`add`?`Turned on ${L.tool(i.label)}. Your workspace can start it now: pick it on Run.`:`Turned off ${L.tool(i.label)}.`)),console.log(` ${Es(a.machine)}`)}let a=gr(t),o=(r.customTools??[]).filter(e=>e.name!==t),s=r.runtimes.filter(e=>e!==a);if(e===`rm`&&!(r.customTools??[]).some(e=>e.name===t)&&!r.runtimes.includes(a))throw new X(`no tool or agent called ${t} here. See ${w} status`);if(e===`add`){let e=js(t,n),r=e.protocol===`acp`?Re(e.command).bin:e.command.split(/\s+/)[0]??``;dr(r)||console.log(wn(`warning: ${r} isn't installed here yet.`)),o=[...o,e],s=[...s,a]}let c=await ks(r,s),l=j();l.machine={...l.machine,customTools:o,runtimes:c.runtimes},Dt(l),console.log(G(e===`add`?`Added ${L.tool(t)}. Your workspace can start it now: pick it on Run.`:`Removed ${L.tool(t)}.`)),console.log(` ${Es(l.machine)}`)}function js(e,t){if(t.acp!==void 0){let n=t.acp.trim();if(!n)throw new X(`${w} add ${e} --acp "<cmd>" (a command that speaks ACP on stdio)`);try{Re(n)}catch(e){throw new X(`--acp: ${e.message}`)}return{name:e,command:n,protocol:`acp`}}let n=t.command?.trim();if(!n)throw new X(cr.some(t=>t.id===e.replace(/-/g,`_`))?`${e} isn't built in yet; give the command to run: ${w} add ${e} --command "<cmd>" (it gets the work packet on stdin)`:`${e} isn't a coding tool bitfolk knows; to add your own: ${w} add ${e} --command "<cmd>" (see ${w} tools)`);return{name:e,command:n}}async function Ms(){let e=j();if(!e.machine)return console.log(`${I(R.idle)} This computer isn't linked.`);let t=e.machine,n=await q(`Unlinking from your workspace`,()=>sr(t)),r=e.machine.name;delete e.machine,Dt(e),console.log(n?G(`Unlinked ${F(r)} from your workspace.`):wn(`Forgot ${r} here, but couldn't reach Bitfolk: press Unlink in Settings, Machines.`))}async function Ns(e,t,n,r,i){let a=j(),o=a.agents.find(e=>e.id===n.agent.id),s=r.runtime??o?.runtime??ke(n.agent.runtime);if(!s)throw new X(`${n.agent.name} is a ${n.agent.runtime} agent, which this runner can't start by itself. Add --runtime claude_code, --runtime codex, or --runtime command --command "..."`);o?.oauth&&o.oauth.refreshToken!==i?.refreshToken&&await sr(o);let c=o?{...o,token:t,oauth:i,api:e,name:n.agent.name}:{id:n.agent.id,name:n.agent.name,slug:At(n.agent.name,a.agents.map(e=>e.slug)),runtime:s,api:e,token:t,oauth:i,...xt};c.oauth||delete c.oauth;let l=_s(c,{...r,runtime:s}),u=Oe[l.runtime].bin(l);a.agents=[...a.agents.filter(e=>e.id!==l.id),l],Dt(a);let d=Ot();d.agents[l.id]?.disabledReason&&(delete d.agents[l.id].disabledReason,kt(d)),console.log(G(`${o?`Updated`:`Added`} ${F(l.name)} ${I(`(${n.workspace?.name??`workspace`}, owner ${n.owner?.name??`?`})`)}`)),console.log(bn([[`objectives`,n.goals.map(e=>e.title).join(`; `)||I(`none yet`)]],2).join(`
|
|
93
|
+
`)),console.log(` ${ys(l)}`),i&&console.log(I(` signed in through your browser; sign-in renews by itself`)),l.repo||console.log(` ${I(`no repo yet: it works in a plain folder. Point it at one with:`)} ${W(`${w} set ${l.slug} --repo <path>`)}`),gs(u)||console.log(` ${wn(`warning: \`${u}\` isn't on PATH yet; install it and sign in before \`${w} up\`.`)}`),console.log(`\nStart it with: ${F(W(`${w} up`))}`)}function Ps(e,t){let n=j();if(!e){if(!n.machine)throw new X(`this computer isn't linked yet: run ${w} login`);if(!Object.keys(t).filter(e=>![`no-color`,`help`,`version`,`agent`,`tool`,`force`].includes(e)).length)throw new X(`what to change? ${w} set --repo <path>, or ${w} set --max-usd 2. See ${w} help set`);return n.machine=Ts(n.machine,t),Dt(n),console.log(Es(n.machine))}let r=jt(n,e);if(!r)throw new X(`no agent called ${e} on this machine`);let i=_s(r,t);n.agents=n.agents.map(e=>e.id===r.id?i:e),Dt(n),console.log(ys(i))}async function Fs(e,t){if(!e)throw new X(`which agent? ${w} rm <agent>`);let n=j(),r=jt(n,e);if(!r)throw new X(`no agent called ${e} on this machine`);let i=mo(r);if(i?.changed&&!t.force)throw new X(`${r.name} has ${i.changed} uncommitted change${i.changed===1?``:`s`} in ${i.dir}. Commit or copy ${i.changed===1?`it`:`them`} first, or run ${w} rm "${r.name}" --agent --force to remove it anyway`);ho(r);let a=await q(`Removing ${r.name}`,()=>sr(r));n.agents=n.agents.filter(e=>e.id!==r.id),Dt(n);let o=Ot();delete o.agents[r.id],kt(o);let s=r.repo&&uo(r.repo,lo(r));console.log(G(`Removed ${F(r.name)} from this machine.${s?I(` Its branch ${lo(r)} is still in ${r.repo}.`):``}`)),console.log(r.oauth?a?I(` This machine is unlinked from it on your workspace too.`):` ${wn(`Couldn't reach Bitfolk to unlink it; press Unlink on its page on your workspace.`)}`:I(` Its token still works; revoke it on your workspace if you're done with this agent.`))}function Is(e,t,n){if(!e)return[[`login`,`link this computer to your workspace`],[`tools`,`see the coding tools found here`],[`help`,`all commands`]];let r=[];return t||r.push([`up`,`take sessions your workspace starts here`]),n[0]&&r.push([`add ${n[0]}`,`turn on another tool`]),e.repo||r.push([`set --repo <path>`,`give sessions a git repo`]),r.push([`check`,`check sign-in and tools`]),r.push([`help`,`all commands`]),r.slice(0,4)}async function Ls(){let e=j(),t=e.machine,n=qo(),r=e=>{console.log(``),console.log(vn(e.map(([e,t])=>[di(e),I(t)]),{indent:2,gap:2}).join(`
|
|
94
94
|
`))};if(!t&&e.agents.length===0){let e=ri({version:O,tagline:`Coding sessions for your workspace`,url:`bitfolk.ai`});return e.length&&console.log(e.join(`
|
|
95
95
|
`)+`
|
|
96
|
-
`),console.log(K(`${I(R.idle)} Not linked yet. Run ${W(`${w} login`)} to link this computer to your workspace, then ${W(`${w} up`)}.`)),r(
|
|
97
|
-
`))}else console.log(K(`${I(R.idle)} This computer isn't linked. ${I(`Agents added here still run with`)} ${W(`${w} up`)}${I(`.`)}`));e.agents.length&&(console.log(`\n${F(L.accent(`Agents added here`))}${I(`:`)}`),console.log(Ci(e.agents.map(
|
|
98
|
-
`))),r(
|
|
96
|
+
`),console.log(K(`${I(R.idle)} Not linked yet. Run ${W(`${w} login`)} to link this computer to your workspace, then ${W(`${w} up`)}.`)),r(Is(void 0,!1,[]))}let i=t?hr().filter(e=>e.supported&&!t.runtimes.includes(e.id)).map(e=>e.id.replace(/_/g,`-`)):[],a=[];if(t){let e;try{let r=await q(`Asking your workspace`,()=>Promise.race([new vt(t.api,()=>ar(t)).me(),new Promise((e,t)=>setTimeout(()=>t(Error(`no answer`)),8e3).unref())]));Object.assign(t,Mt(t,r)),e=r.queued?L.accent(`${r.queued} waiting`)+(n?``:I(` · run ${w} up to take them`)):I(`none waiting`)}catch(t){e=t instanceof J&&t.ended?L.err(`sign-in ended; run ${w} login`):t instanceof k&&t.status===401?L.err(`unlinked; run ${w} login`):L.warn(`couldn't reach your workspace (${w} check)`)}console.log(K(`${F(L.accent(t.name))} ${I(`on`)} ${F(t.workspace??`your workspace`)}`)),a.push([`tools`,vi(t.runtimes)+(i.length?I(` ${B(`off: ${i.join(`, `)}`)}`):``)]),a.push([`sessions`,e]),a.push([`up`,n?`${L.ok(R.dot)} ${L.ok(`running`)} ${I(`(pid ${n})`)}`:`${I(R.idle)} ${I(`not running`)}`]),a.push([`limits`,`${L.money(`$${t.maxUsdPerRun}`)}${I(`/session · `)}${t.maxRunsPerHour}${I(`/h · `)}${t.timeoutMin}${I(` min · `)}${t.permission}`]),a.push([`repo`,t.repo??I(`none: builders work in plain folders`)]),console.log(bn(a,2).join(`
|
|
97
|
+
`))}else console.log(K(`${I(R.idle)} This computer isn't linked. ${I(`Agents added here still run with`)} ${W(`${w} up`)}${I(`.`)}`));e.agents.length&&(console.log(`\n${F(L.accent(`Agents added here`))}${I(`:`)}`),console.log(Ci(e.agents.map(vs),2).flat().join(`
|
|
98
|
+
`))),r(Is(t,!!n,i))}async function Rs(){let e=j(),t=0,n=0,r=null,i=``,a=e=>{r?.stop(),r=null,console.log(K(i+e,i.length+2))},o=(e,r)=>{e?t++:n++,a(e?`${L.ok(R.ok)} ${r}`:`${L.err(R.fail)} ${L.err(r)}`)},s=e=>a(`${I(R.skip)} ${I(e)}`),c=e=>{r?.stop(),r=null,i=``,console.log(`\n${F(e)}`),i=` `},l=e=>{r&&r.stop(),r=Vn(e.replace(/\.\.\.$/,``),{quiet:!0})};console.log(K(`${F(L.accent(`bitfolk check`))} ${I(`· v${O} · node ${process.version}`)}`)),console.log(K(I(`config ${Tt()}`))),o(!!dr(`git`),`git ${dr(`git`)?`found`:`not found (needed for repos and worktrees)`}`);let u=hr(),d=[];for(let e of cr.filter(e=>e.supported)){let t=u.find(t=>t.id===e.id);t?o(!0,`${L.tool(e.label)} found ${I(`(${t.path})`)}`):d.push(e.label)}d.length&&s(`not found: ${d.join(`, `)}`),(process.env.HTTPS_PROXY||process.env.https_proxy||process.env.HTTP_PROXY)&&!process.env.NODE_USE_ENV_PROXY&&s(`HTTPS_PROXY is set, but Node ignores it unless NODE_USE_ENV_PROXY=1 (Node 22.21+). Set it if bitfolk can't connect.`);let f=e.machine?.api??process.env.BITFOLK_API;f&&f.replace(/\/+$/,``)!==`https://bitfolk.ai`&&s(`talking to ${f}, not ${gt}`);let p=e.machine;if(!p)s(`this computer isn't linked (${w} login)`);else{c(p.name),l(`checking ${p.name}'s link to your workspace...`);try{let e=await new vt(p.api,()=>ar(p)).me();Mt(p,e),o(!0,`linked to ${F(e.workspace?.name??`your workspace`)}, ${e.queued} session${e.queued===1?``:`s`} waiting`)}catch(e){o(!1,e instanceof J&&e.ended?`sign-in ended; run ${w} login`:e instanceof k&&e.status===401?`unlinked; run ${w} login`:e.message)}for(let e of p.runtimes){let t;try{t=zo({...p,command:void 0},e).acp}catch{}if(e.startsWith(`custom:`)){let n=p.customTools?.find(t=>gr(t.name)===e),r=t?.bin??n?.command.trim().split(/\s+/)[0]??``;o(!!n&&!!dr(r),`${L.tool(Y(e))} sessions run \`${n?.command??`(not set up here)`}\`${t?` over ACP`:``}`)}else{let t=cr.find(t=>t.id===e),n=t?fr(t):null;if(o(!!n,`${L.tool(Y(e))} sessions${n?I(` (${n}${t?.acp?`, via ACP`:``})`):` need it installed`}`),!n)continue}let n=t&&!p.command?dr(t.bin):null;t&&n&&(l(`asking ${Y(e)} to say hello over ACP...`),await zs(Y(e),{...t,bin:n},o))}p.repo&&o(!!co(p.repo),`repo ${p.repo}`),process.platform===`darwin`&&s(`${w} up keeps this Mac awake, but closing the lid on battery still sleeps it and pauses sessions.`)}for(let t of e.agents){c(t.name),l(`checking ${t.name}...`);try{let e=await new _t(t.api,t.oauth?()=>ar(t):t.token).setup();o(!0,`${t.oauth?`signed in`:`token works`} (${e.goals.length} objective${e.goals.length===1?``:`s`})`)}catch(e){o(!1,e instanceof J&&e.ended?`sign-in ended; add it again with ${w} add <token from its page>`:e instanceof k&&e.status===401?`token rejected`:e.message)}let e=Oe[t.runtime].bin(t);o(gs(e),`runtime ${L.tool(t.runtime)} ${I(`(${e||`no command`})`)}`),t.repo&&o(!!co(t.repo),`repo ${t.repo}`)}r?.stop(),i=``;let m=`${t} check${t===1?``:`s`} passed`;console.log(n?`\n${L.err(R.fail)} ${L.err(F(`${n} problem${n===1?``:`s`}`))}${I(`, ${m}`)}`:`\n${L.ok(R.ok)} ${L.ok(F(`All good`))}${I(`: ${m}`)}`),n&&(process.exitCode=1)}async function zs(e,t,n){try{let r=await ht(t,{timeoutMs:5e3,version:O}),i=[r.agentInfo?.name,r.agentInfo?.version].filter(Boolean).join(` `)||`an unnamed agent`;if(r.protocolVersion!==1)return n(!1,`${e} speaks ACP v${String(r.protocolVersion)}; this runner speaks v1`);let a=r.authMethods.map(e=>e.description??e.name??e.id).filter(Boolean);n(!0,`${L.tool(e)} answers over ACP v1 ${I(`(${i})${a.length?`; sign-in: ${a.join(`; `)}`:``}`)}`)}catch(t){n(!1,`${e} didn't answer ACP initialize: ${t.message}`)}}const Bs=(e,t,n)=>e?`on`:process.platform===`darwin`&&n.machine&&t[`let-sleep`]?`off`:void 0;async function Vs(e,t,n={}){let r=j();if(n.sessionsOnly&&(r.agents=[]),e){let t=jt(r,e);if(!t)throw new X(`no agent called ${e}. See ${w} status`);r.agents=[t]}let i=Jo(),a=null,o=e=>a?a.log(e):console.log(e),s=new Uo(r,{out:o,liveFooter:An()&&!!process.stdout.isTTY}),c=yr({platform:process.platform,linked:!!r.machine,letSleep:t[`let-sleep`]})?br():null;if(r.machine){let e=r.machine,t=await Promise.race([new vt(e.api,()=>ar(e)).me().catch(()=>null),new Promise(e=>setTimeout(()=>e(null),3e3).unref())]);t&&(r.machine=Mt(e,t))}(r.machine||r.agents.length)&&(await oi(Di({version:O,machine:r.machine,agents:r.agents,maxConcurrent:r.maxConcurrent,awake:Bs(c,t,r)}),void 0,void 0,void 0,()=>{c?.(),i()}),console.log(``));let l={machine:r.machine,agents:r.agents,maxConcurrent:r.maxConcurrent,awake:Bs(c,t,r),version:O};a=Un((e,t)=>ji(s,l,{frame:e,...t}),{intervalMs:()=>{let e=s.status();return e.running||e.stopping||e.checking?100:250}});let u=()=>{a?.stop(),s.killAll(),c?.()},d=0,f=null,p=e=>{let t=Date.now();d&&t-d<1e3||(d&&(u(),i(),process.exit(e===`SIGTERM`?143:130)),d=t,f=e,o(s.running?`\n${L.warn(R.idle)} ${L.warn(`Stopping:`)} letting running sessions wrap up and reporting them as interrupted. ${I(`Ctrl-C again to quit now.`)}`:`\n${L.warn(R.idle)} ${L.warn(`Stopping.`)}`),s.stop())};process.on(`SIGINT`,p),process.on(`SIGTERM`,p),process.on(`SIGHUP`,()=>{u(),i(),process.exit(129)}),process.on(`exit`,u);try{let e=await s.run({once:t.once});if(a.stop(),t.once&&e.length){let t=e.reduce((e,t)=>e+t.durationMs,0);console.log(`${L.ok(R.ok)} ${F(`${e.length} run${e.length===1?``:`s`}`)}${I(`, ${Fo(t)} in all.`)}`)}}finally{a.stop(),c?.(),i(),f&&(process.exitCode=f===`SIGINT`?130:143)}}async function Hs(e,t){let n=e.join(` `).trim();if(!n&&!process.stdin.isTTY){let e=[];for await(let t of process.stdin)e.push(t);n=Buffer.concat(e).toString(`utf8`).trim()}if(n.length<3)throw new X(`describe the bug: ${w} bug "what you did and what happened"`);let r=await q(`Sending your report`,()=>fetch(new URL(`/api/bugs`,bs(t)),{method:`POST`,headers:{"content-type":`application/json`,"user-agent":`bitfolk-cli/${O}`},body:JSON.stringify({platform:`cli`,body:n.slice(0,5e3),email:t.email,app_version:`bitfolk-cli ${O}`,device:`${process.platform} ${process.arch}, node ${process.version}`})})),i=await r.json().catch(()=>({}));if(!r.ok)throw Error(i.error??`report failed (${r.status})`);console.log(G(`Thanks. The bug is on our list, and we read every report.`))}const Us=e=>w===`bitfolk`?e:e.replace(/(?<!npx )\bbitfolk (?=[a-z<-])/g,`${w} `);function Ws(){let e=j(),t=e.machine;return{isAgent:t=>e.agents.some(e=>e.slug===t.toLowerCase()||e.name.toLowerCase()===t.toLowerCase()),isTool:e=>cr.some(t=>t.id===e.replace(/-/g,`_`))||!!t?.customTools?.some(t=>t.name===e)||!!t?.runtimes.includes(gr(e)),envToken:process.env.BITFOLK_TOKEN}}async function Gs(e){let{values:t,positionals:n}=d({args:e,options:ms,allowPositionals:!0,strict:!0}),r=t;r[`no-color`]&&Yt({depth:0,fancy:!1,links:!1});let i=ta(n,r,Ws());switch(i.kind){case`version`:return console.log(O);case`help`:{let e={version:O,defaults:xt,api:gt};if(!i.topic)return console.log(Us(fi({...e,rows:process.stdout.isTTY?process.stdout.rows:void 0})));let t=pi(Yi(i.topic),e);if(!t)throw new X(`no help for "${i.topic}". Try ${w} --help`);return console.log(Us(t))}case`status`:return Ls();case`login`:return ws(r);case`logout`:return Ms();case`up`:return Vs(i.agent,r);case`start`:return va(j().machine,i.words,r,{upRunning:!!qo(),runHere:()=>Vs(void 0,{...r,once:!0},{sessionsOnly:!0})});case`sessions`:return ba(j().machine);case`logs`:return xa(j().machine,i.session,r);case`cancel`:return Sa(j().machine,i.session);case`goals`:return Ca(j().machine);case`chat`:return qa(j().machine,i.chat,i.words,r);case`board`:return Ja(j().machine,i.goal,r);case`agents`:return Ya(j().machine,r);case`approvals`:return Xa(j().machine,r);case`decide`:return Za(j().machine,i.decision,i.request,r);case`tools`:return Ds();case`tool`:return As(i.action,i.name,r);case`agent-add`:return xs(i.token,r);case`agent-rm`:return Fs(i.key,r);case`set-machine`:return Ps(void 0,r);case`set-agent`:return Ps(i.key,r);case`share`:{let e=j().machine;if(!e)throw new X(`link this computer first: ${w} login`);let t=hs(i.port,`port`,1,65535);if(!t||!Number.isInteger(t))throw new X(`${w} share <port> [--name "Deal radar"] [--run "<command>"] [--keep <hours>] [--personal | --public]`);let n=hs(r.keep,`keep`,1,72);if(r.approval&&!r.public)throw new X(`--approval goes with --public`);if(r.personal&&r.public)throw new X(`pick one: --personal (only you) or --public (anyone with the link)`);return ps(e,t,{name:r.name,public:r.public,personal:r.personal,run:r.run,keep:n,approval:r.approval})}case`share-stop`:{let e=hs(i.port,`port`,1,65535);if(!e||!Number.isInteger(e))throw new X(`${w} share stop <port>`);return ds(e)}case`check`:return Rs();case`bug`:return Hs(i.words,r);case`usage`:throw new X(Us(i.message));case`unknown`:{let e=Qi(i.command);throw new X(e?`unknown command "${i.command}". Did you mean ${w} ${e}?`:`unknown command "${i.command}". Try ${w} --help`)}}}const Ks=(e,t)=>{console.error(Dn(K(Tn(e,`bitfolk: `)))),process.exit(t)};Gs(process.argv.slice(2)).catch(e=>{let t=e.code;if(t===`ERR_PARSE_ARGS_UNKNOWN_OPTION`){let t=/'([^']+)'/.exec(e.message)?.[1]??`that option`;Ks(`unknown option ${t}. Try ${w} --help`,2)}(t===`ERR_PARSE_ARGS_INVALID_OPTION_VALUE`||t===`ERR_PARSE_ARGS_UNEXPECTED_POSITIONAL`)&&Ks(`${e.message.split(`.`)[0]}. Try ${w} --help`,2),e instanceof X&&Ks(e.message,2),e instanceof k&&Ks(e.status===401?`that token was rejected (revoked, or the agent was archived)`:e.message,1),Ks(e.message,1)});export{};
|
package/package.json
CHANGED