balladeer 1.0.0 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +53 -32
- package/dist/agent.d.ts +5 -0
- package/dist/agent.js +13 -1
- package/dist/cli.d.ts +16 -0
- package/dist/cli.js +181 -24
- package/dist/client.d.ts +24 -2
- package/dist/client.js +34 -3
- package/dist/commands/affected.js +8 -7
- package/dist/commands/check-seals.js +4 -4
- package/dist/commands/discover.js +16 -7
- package/dist/commands/explain.d.ts +1 -1
- package/dist/commands/explain.js +1 -1
- package/dist/commands/invite.js +2 -1
- package/dist/commands/prepare.d.ts +74 -0
- package/dist/commands/prepare.js +218 -0
- package/dist/commands/propose.d.ts +10 -0
- package/dist/commands/propose.js +29 -6
- package/dist/commands/repositories.js +1 -0
- package/dist/commands/session.d.ts +35 -0
- package/dist/commands/session.js +131 -0
- package/dist/commands/setup.d.ts +29 -0
- package/dist/commands/setup.js +302 -92
- package/dist/commands/status.d.ts +16 -0
- package/dist/commands/status.js +106 -23
- package/dist/commands/touch-map.js +2 -2
- package/dist/commands/whoami.js +2 -1
- package/dist/conventions.d.ts +9 -1
- package/dist/conventions.js +9 -1
- package/dist/copy.d.ts +83 -7
- package/dist/copy.js +226 -29
- package/dist/desktop-config.d.ts +85 -0
- package/dist/desktop-config.js +217 -0
- package/dist/git.d.ts +15 -0
- package/dist/git.js +23 -0
- package/dist/legacy.d.ts +41 -0
- package/dist/legacy.js +143 -0
- package/dist/local-time.d.ts +66 -0
- package/dist/local-time.js +84 -0
- package/dist/mcp-config.d.ts +10 -0
- package/dist/mcp-config.js +8 -4
- package/dist/session.d.ts +84 -0
- package/dist/session.js +135 -0
- package/dist/store.d.ts +11 -1
- package/dist/store.js +18 -6
- package/dist/wire.d.ts +95 -4
- package/dist/wire.js +2 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -19,11 +19,12 @@ The agent reads the setup instructions, runs the command, and hands you the two
|
|
|
19
19
|
do. To run it yourself instead:
|
|
20
20
|
|
|
21
21
|
```sh
|
|
22
|
-
npx balladeer@
|
|
22
|
+
npx -y balladeer@latest setup
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
Use `@latest` so a new session picks up the current command. Balladeer tells an older command when
|
|
26
|
+
an update is available. Run `npx -y balladeer@latest setup --refresh` to update an existing
|
|
27
|
+
repository connection and its instructions without replacing other tools' settings.
|
|
27
28
|
|
|
28
29
|
Run it inside the repository you want to protect. It prints the boundary explanation below, then one
|
|
29
30
|
step at a time, and it exits within seconds rather than blocking on anything.
|
|
@@ -33,6 +34,12 @@ connects this coding agent, records the CI identity and opens the pull request w
|
|
|
33
34
|
and hands you the playbook for discovering this repository's own promises. The two things it never
|
|
34
35
|
does are sign you in and agree to a promise; both of those are yours, in a browser.
|
|
35
36
|
|
|
37
|
+
If you are joining an existing workspace, use `npx -y balladeer@latest setup --existing`. It
|
|
38
|
+
completes browser onboarding without adding a repository or changing CI. When this checkout is
|
|
39
|
+
already enrolled it connects this machine and writes its MCP configuration; when it is absent, setup
|
|
40
|
+
leaves the checkout unchanged and Balladeer shows that enforcement tracking is unavailable until an
|
|
41
|
+
administrator connects a repository.
|
|
42
|
+
|
|
36
43
|
## Requirements
|
|
37
44
|
|
|
38
45
|
- Node 22 or newer. The command has no runtime dependencies at all: it uses Node builtins and
|
|
@@ -57,19 +64,20 @@ test output.
|
|
|
57
64
|
The verify job in your CI runs your tests with your checkout and has no Balladeer credential. A
|
|
58
65
|
separate publish job, with no checkout, sends only those outcomes and hashes.
|
|
59
66
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
67
|
+
Browser approval creates a temporary setup session limited by that person's workspace role. A
|
|
68
|
+
contributor can connect this machine's coding agent to an already-enrolled repository and propose
|
|
69
|
+
promises; an administrator can also add repositories, connect CI, and invite teammates. The session
|
|
70
|
+
cannot approve, activate, rotate, revoke, or remove anything. It lasts a day, each use buys it
|
|
71
|
+
another day, and it is gone seven days after approval however much it was used. When setup connects
|
|
72
|
+
an enrolled repository it also issues that machine a coding-agent credential. That connection has no
|
|
73
|
+
expiry: it lets the agent read the promises your team has approved, propose new ones, and propose
|
|
74
|
+
replacing, retiring, excepting, or reassigning one you already have; every one of those waits for a
|
|
75
|
+
named person to decide it. It can also carry out one of those acts for you, and only this way: you
|
|
76
|
+
open a Balladeer page in your own browser, read what the act is, sign it off there, and read back
|
|
77
|
+
the one-time code that page gives you. That code covers that one act on that one thing, once, and
|
|
78
|
+
Balladeer records you as the person who took it and the connection as the messenger. Without a code
|
|
79
|
+
you signed, the connection cannot approve, activate, grant, transfer, retire, or delete anything.
|
|
80
|
+
You can revoke either one at any time in Balladeer, and the setup session also ends on its own.
|
|
73
81
|
|
|
74
82
|
Adding the workflow puts a check on pull requests into your default branch. That check is advisory
|
|
75
83
|
on Balladeer's side; whether it blocks a merge is your own branch protection.
|
|
@@ -107,20 +115,22 @@ your own branch protection.
|
|
|
107
115
|
|
|
108
116
|
## Commands
|
|
109
117
|
|
|
110
|
-
| Command
|
|
111
|
-
|
|
|
112
|
-
| `balladeer setup` | Run all five steps, then report what a person still has to do
|
|
113
|
-
| `balladeer repositories` | List what this machine could add, marking the one you are in and the ones in
|
|
114
|
-
| `balladeer invite` | Invite teammates by email, and say per address whether the email went
|
|
115
|
-
| `balladeer status` | Report this repository over its agent connection, and the workspace
|
|
116
|
-
| `balladeer status <id>` | Report one promise: whether it is holding, and if not, what the run reported
|
|
117
|
-
| `balladeer
|
|
118
|
-
| `balladeer
|
|
119
|
-
| `balladeer
|
|
120
|
-
| `balladeer
|
|
121
|
-
| `balladeer
|
|
122
|
-
| `balladeer
|
|
123
|
-
| `balladeer
|
|
118
|
+
| Command | What it does |
|
|
119
|
+
| -------------------------------------- | -------------------------------------------------------------------------------- |
|
|
120
|
+
| `npx -y balladeer@latest setup` | Run all five steps, then report what a person still has to do |
|
|
121
|
+
| `npx -y balladeer@latest repositories` | List what this machine could add, marking the one you are in and the ones in |
|
|
122
|
+
| `npx -y balladeer@latest invite` | Invite teammates by email, and say per address whether the email went |
|
|
123
|
+
| `npx -y balladeer@latest status` | Report this repository over its agent connection, and the workspace |
|
|
124
|
+
| `npx -y balladeer@latest status <id>` | Report one promise: whether it is holding, and if not, what the run reported |
|
|
125
|
+
| `npx -y balladeer@latest prepare <id>` | Prepare a promise's one-time qualification setup, and write it where CI reads it |
|
|
126
|
+
| `npx -y balladeer@latest propose` | Propose one promise from a proposal file, over that connection |
|
|
127
|
+
| `npx -y balladeer@latest discover` | File a whole catalog, up to ten promises from one file, behind one review link |
|
|
128
|
+
| `npx -y balladeer@latest touch-map` | Record which files each promise's verifier runs, on this machine only |
|
|
129
|
+
| `npx -y balladeer@latest affected` | Say which promises the files you name touch, out of that record |
|
|
130
|
+
| `npx -y balladeer@latest session` | Print the id for this piece of work, and the line to write into the commit |
|
|
131
|
+
| `npx -y balladeer@latest mcp` | Forward one MCP session over stdio using this repository's connection |
|
|
132
|
+
| `npx -y balladeer@latest explain` | What Balladeer can and cannot see, where to watch it, and what leaving costs |
|
|
133
|
+
| `npx -y balladeer@latest whoami` | Report the stored session's workspace, role, scopes, and expiry |
|
|
124
134
|
|
|
125
135
|
`--json` emits one object per step on stdout and nothing else. `--wait` makes `setup` poll for the
|
|
126
136
|
approval instead of exiting; without it the command exits and a later run finishes the pairing.
|
|
@@ -129,7 +139,7 @@ approval instead of exiting; without it the command exits and a later run finish
|
|
|
129
139
|
to be the one you are standing in, because connecting a coding agent writes files into a working
|
|
130
140
|
tree and connecting CI pushes a branch to a remote. The rest are added to the workspace and nothing
|
|
131
141
|
more, and the run says so for each of them; somebody runs setup in a checkout of each one to finish
|
|
132
|
-
it. `balladeer repositories` is what you read first to decide which ones to name.
|
|
142
|
+
it. `npx -y balladeer@latest repositories` is what you read first to decide which ones to name.
|
|
133
143
|
|
|
134
144
|
`invite` takes one or more addresses and `--role contributor|viewer|administrator`, defaulting to
|
|
135
145
|
contributor. It goes over the setup session's `workspace:invite` grant, so it works only for a
|
|
@@ -148,7 +158,18 @@ once they exist, and what stopping costs: your tests are yours, they keep runnin
|
|
|
148
158
|
administrator can download everything Balladeer holds at any time.
|
|
149
159
|
|
|
150
160
|
Exit codes: 0 progress reported truthfully, 2 pairing expired or denied or already claimed, 3 this
|
|
151
|
-
copy is too old for the server, 4 usage or credential store problem, 5 transport or server failure
|
|
161
|
+
copy is too old for the server, 4 usage or credential store problem, 5 transport or server failure,
|
|
162
|
+
6 an earlier Balladeer is still installed on this machine and nothing was changed.
|
|
163
|
+
|
|
164
|
+
## An earlier Balladeer on the same machine
|
|
165
|
+
|
|
166
|
+
A machine that ran the earlier Balladeer still carries it: that client registers an MCP server named
|
|
167
|
+
`balladeer` for the whole machine and installs a session hook of its own, and neither comes off when
|
|
168
|
+
this one goes on. Setup finds it before it writes anything and stops with one instruction. If you
|
|
169
|
+
used the earlier Balladeer on this machine: run `balladeer uninstall`, then the removal command it
|
|
170
|
+
prints, then continue. That uninstall is reversible, it backs up `~/.balladeer`, and it leaves the
|
|
171
|
+
old binary in place. `npx -y balladeer@latest setup --force` runs anyway, for whoever has decided to
|
|
172
|
+
keep both.
|
|
152
173
|
|
|
153
174
|
## Revoking
|
|
154
175
|
|
package/dist/agent.d.ts
CHANGED
|
@@ -124,3 +124,8 @@ export declare function safeJson(response: Response): Promise<unknown>;
|
|
|
124
124
|
export declare function updateLine(payload: unknown): string;
|
|
125
125
|
/** One field of a tool result, when it is a string. Never a guess. */
|
|
126
126
|
export declare function structuredString(structured: unknown, field: string): string | undefined;
|
|
127
|
+
/** Render the server's live enforcement limitation once per CLI operation. */
|
|
128
|
+
export declare function reportAgentEnforcementWarning(call: AgentToolCall, output: Readonly<{
|
|
129
|
+
json: boolean;
|
|
130
|
+
write: (text: string) => void;
|
|
131
|
+
}>): void;
|
package/dist/agent.js
CHANGED
|
@@ -21,7 +21,8 @@ const REPOSITORY_NAME = /^[A-Za-z0-9._-]{1,39}\/[A-Za-z0-9._-]{1,100}$/;
|
|
|
21
21
|
export function selectAgent(agents, controlPlane, repositoryId, currentRepository) {
|
|
22
22
|
const here = agents.filter((agent) => agent.controlPlane === controlPlane);
|
|
23
23
|
if (repositoryId !== undefined) {
|
|
24
|
-
const named = here.find((agent) => agent.repositoryId === repositoryId
|
|
24
|
+
const named = here.find((agent) => agent.repositoryId === repositoryId ||
|
|
25
|
+
agent.repository?.toLowerCase() === repositoryId.toLowerCase());
|
|
25
26
|
if (named === undefined) {
|
|
26
27
|
return {
|
|
27
28
|
kind: "refused",
|
|
@@ -207,3 +208,14 @@ export function structuredString(structured, field) {
|
|
|
207
208
|
const value = structured[field];
|
|
208
209
|
return typeof value === "string" ? value : undefined;
|
|
209
210
|
}
|
|
211
|
+
/** Render the server's live enforcement limitation once per CLI operation. */
|
|
212
|
+
export function reportAgentEnforcementWarning(call, output) {
|
|
213
|
+
if (call.kind !== "result" && call.kind !== "tool_refusal")
|
|
214
|
+
return;
|
|
215
|
+
const message = structuredString(call.structured, "enforcementWarning");
|
|
216
|
+
if (message === undefined)
|
|
217
|
+
return;
|
|
218
|
+
output.write(output.json
|
|
219
|
+
? `${JSON.stringify({ step: "warning", code: "enforcement_unavailable", message })}\n`
|
|
220
|
+
: `WARNING ${message}\n`);
|
|
221
|
+
}
|
package/dist/cli.d.ts
CHANGED
|
@@ -6,6 +6,16 @@ type Parsed = Readonly<{
|
|
|
6
6
|
wait: boolean;
|
|
7
7
|
/** Repair the files a previous setup wrote, and do nothing else. */
|
|
8
8
|
refresh: boolean;
|
|
9
|
+
/** `setup --force`: set up even though an earlier Balladeer is still installed. */
|
|
10
|
+
force: boolean;
|
|
11
|
+
/** `setup --existing`: connect only when this checkout is already enrolled. */
|
|
12
|
+
existingOnly: boolean;
|
|
13
|
+
/**
|
|
14
|
+
* `setup --claude-desktop` / `--no-claude-desktop`. Undefined is neither
|
|
15
|
+
* asked for nor refused, which is the ordinary run: connect the chat client
|
|
16
|
+
* where it is installed and say nothing where it is not.
|
|
17
|
+
*/
|
|
18
|
+
claudeDesktop: boolean | undefined;
|
|
9
19
|
repo: string | undefined;
|
|
10
20
|
file: string | undefined;
|
|
11
21
|
repository: string | undefined;
|
|
@@ -21,6 +31,12 @@ type Parsed = Readonly<{
|
|
|
21
31
|
owner: string | undefined;
|
|
22
32
|
/** `check-seals --install-hook`: write the optional pre-push hook. */
|
|
23
33
|
installHook: boolean;
|
|
34
|
+
/** `session --new`: start a different session even though one is current. */
|
|
35
|
+
fresh: boolean;
|
|
36
|
+
/** `session --record`: read the trailer out of HEAD and record that commit. */
|
|
37
|
+
record: boolean;
|
|
38
|
+
/** `prepare --again`: mint a replacement packet and invalidate the unspent one. */
|
|
39
|
+
again: boolean;
|
|
24
40
|
/** `check-seals --runner`: where the pinned runner is, when it has moved. */
|
|
25
41
|
runner: string | undefined;
|
|
26
42
|
createWorkspace: string | undefined;
|
package/dist/cli.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { realpathSync } from "node:fs";
|
|
2
3
|
import { resolve } from "node:path";
|
|
3
4
|
import { fileURLToPath } from "node:url";
|
|
4
5
|
import { runAffected } from "./commands/affected.js";
|
|
@@ -7,19 +8,23 @@ import { runDiscover } from "./commands/discover.js";
|
|
|
7
8
|
import { runExplain } from "./commands/explain.js";
|
|
8
9
|
import { parseRole, runInvite } from "./commands/invite.js";
|
|
9
10
|
import { runMcp } from "./commands/mcp.js";
|
|
11
|
+
import { runPrepare } from "./commands/prepare.js";
|
|
10
12
|
import { runPropose } from "./commands/propose.js";
|
|
11
13
|
import { runRepositories } from "./commands/repositories.js";
|
|
12
14
|
import { CREATE_WORKSPACE_MAX_LENGTH, runSetup } from "./commands/setup.js";
|
|
15
|
+
import { runSession } from "./commands/session.js";
|
|
13
16
|
import { runStatus } from "./commands/status.js";
|
|
14
17
|
import { runTouchMap } from "./commands/touch-map.js";
|
|
15
18
|
import { runWhoami } from "./commands/whoami.js";
|
|
16
19
|
import { updateNotice } from "./currency.js";
|
|
17
20
|
import { StoreError, normalizeControlPlane } from "./store.js";
|
|
18
|
-
import { CLI_VERSION, DEFAULT_CONTROL_PLANE } from "./wire.js";
|
|
21
|
+
import { CLI_INVOCATION, CLI_VERSION, DEFAULT_CONTROL_PLANE } from "./wire.js";
|
|
19
22
|
const USAGE = `balladeer ${CLI_VERSION}
|
|
20
23
|
|
|
21
|
-
|
|
22
|
-
[--create-workspace <name>] [--refresh]
|
|
24
|
+
${CLI_INVOCATION} setup [--repository owner/name]... [--json] [--wait] [--control-plane <url>]
|
|
25
|
+
[--create-workspace <name>] [--refresh] [--force]
|
|
26
|
+
[--existing]
|
|
27
|
+
[--claude-desktop | --no-claude-desktop]
|
|
23
28
|
Pair this session, add this repository, connect this coding agent and CI,
|
|
24
29
|
and report what a person still has to do. Name --repository more than once
|
|
25
30
|
to add other repositories to the same workspace; each of those is added and
|
|
@@ -27,21 +32,32 @@ const USAGE = `balladeer ${CLI_VERSION}
|
|
|
27
32
|
--create-workspace carries a name to the approval page, where a person
|
|
28
33
|
signs in and creates the workspace themselves; this command never creates
|
|
29
34
|
one.
|
|
35
|
+
--existing is for an invited teammate and never enrolls repositories or changes CI.
|
|
36
|
+
If this repository is already connected, connect this machine; otherwise
|
|
37
|
+
continue in the browser while an administrator connects it.
|
|
30
38
|
--refresh does one thing and talks to nobody: it rewrites this
|
|
31
|
-
repository's balladeer entry in .mcp.json
|
|
32
|
-
block to the current form, leaves every
|
|
33
|
-
and prints what it changed. Run it when
|
|
34
|
-
available.
|
|
39
|
+
repository's balladeer entry in .mcp.json, its Balladeer instructions
|
|
40
|
+
block and its Claude desktop chat entry to the current form, leaves every
|
|
41
|
+
other entry in those files alone, and prints what it changed. Run it when
|
|
42
|
+
Balladeer says a newer version is available.
|
|
43
|
+
--force sets up even though an earlier Balladeer is still installed on
|
|
44
|
+
this machine. Without it, a run that finds the older client's machine-wide
|
|
45
|
+
MCP entry or session hook stops and says to remove it first.
|
|
46
|
+
Claude desktop chat is connected too, under a key named for this
|
|
47
|
+
repository, wherever the app is installed. --claude-desktop asks for it by
|
|
48
|
+
name, so a machine with no Claude desktop on it says so instead of
|
|
49
|
+
skipping quietly; --no-claude-desktop leaves that file alone entirely.
|
|
50
|
+
Quit and reopen the app afterwards: it reads its configuration at startup.
|
|
35
51
|
|
|
36
|
-
|
|
52
|
+
${CLI_INVOCATION} repositories [--json] [--control-plane <url>]
|
|
37
53
|
List the repositories this machine's GitHub account can see, marking the
|
|
38
54
|
one you are standing in and the ones Balladeer already has.
|
|
39
55
|
|
|
40
|
-
|
|
56
|
+
${CLI_INVOCATION} invite <email>... [--role contributor|viewer|administrator] [--json]
|
|
41
57
|
Invite teammates into this workspace by email, and say per address whether
|
|
42
58
|
the invitation went. Inviting is an administrator's act.
|
|
43
59
|
|
|
44
|
-
|
|
60
|
+
${CLI_INVOCATION} status [<promise id>] [--repo owner/name] [--repository <uuid>] [--json]
|
|
45
61
|
[--control-plane <url>]
|
|
46
62
|
Report this repository over its agent connection, and every repository in
|
|
47
63
|
the workspace when a setup session is still live. Named with a promise id,
|
|
@@ -50,17 +66,30 @@ const USAGE = `balladeer ${CLI_VERSION}
|
|
|
50
66
|
the agreed meaning that run was checking. That is the form the line on a
|
|
51
67
|
broken promise tells a person to run first.
|
|
52
68
|
|
|
53
|
-
|
|
69
|
+
${CLI_INVOCATION} propose --file <path> [--repo owner/name] [--repository <uuid>] [--json]
|
|
54
70
|
Propose one promise from a proposal file, over this repository's agent
|
|
55
71
|
connection. A named person still agrees to it.
|
|
56
72
|
|
|
57
|
-
|
|
73
|
+
${CLI_INVOCATION} prepare <promise id> [--again] [--repo owner/name] [--repository <uuid>]
|
|
74
|
+
[--json] [--control-plane <url>]
|
|
75
|
+
Prepare the one-time qualification setup for a promise whose meaning is
|
|
76
|
+
agreed and which nothing is checking yet, and write it to
|
|
77
|
+
.continuity/qualification/<promise id>.json, which is where the sealed run
|
|
78
|
+
reads it. No sign-off and no code: agreeing the meaning was the person's
|
|
79
|
+
act, and building the check that proves it is yours. Then build the
|
|
80
|
+
verifier, seal it, and push to the default branch; protection starts by
|
|
81
|
+
itself when that run qualifies. It is one-time, so while nobody has
|
|
82
|
+
published against the packet already prepared this refuses and says who
|
|
83
|
+
prepared it and when. --again prepares a replacement and invalidates that
|
|
84
|
+
earlier packet: a run publishing its identities afterwards is refused.
|
|
85
|
+
|
|
86
|
+
${CLI_INVOCATION} discover --file <path> [--repo owner/name] [--repository <uuid>]
|
|
58
87
|
[--owner <membership id>] [--json]
|
|
59
88
|
Propose a whole discovered catalog, up to ten promises from one file, each
|
|
60
89
|
owned by whoever paired this machine, and print one link that opens all of
|
|
61
90
|
them. A named person still agrees to every one.
|
|
62
91
|
|
|
63
|
-
|
|
92
|
+
${CLI_INVOCATION} check-seals [--json] [--runner <path>] [--install-hook]
|
|
64
93
|
Say whether what you are about to push would break a promise's seal. It
|
|
65
94
|
prints nothing and exits 0 when it would not, and names the promise, its
|
|
66
95
|
owner, its page and the line that seals it again when it would. It runs
|
|
@@ -68,34 +97,45 @@ const USAGE = `balladeer ${CLI_VERSION}
|
|
|
68
97
|
writes .git/hooks/pre-push so every push asks first; nothing installs it
|
|
69
98
|
for you, and deleting that file removes it.
|
|
70
99
|
|
|
71
|
-
|
|
100
|
+
${CLI_INVOCATION} touch-map [--json]
|
|
72
101
|
Run this repository's promise verifiers under coverage and write down
|
|
73
102
|
which files each one actually executed, to .continuity/touch-map.json.
|
|
74
103
|
It runs offline and the map stays on this machine: Balladeer is never
|
|
75
104
|
sent it. Node verifiers only in this release.
|
|
76
105
|
|
|
77
|
-
|
|
106
|
+
${CLI_INVOCATION} affected <paths...> [--json]
|
|
78
107
|
Say which promises the named files touch, out of that map, marking any
|
|
79
108
|
answer whose verifier has changed since the map was built. It reads one
|
|
80
109
|
local file and contacts nothing.
|
|
81
110
|
|
|
82
|
-
|
|
111
|
+
${CLI_INVOCATION} session [--new] [--record] [--repo owner/name] [--repository <uuid>] [--json]
|
|
112
|
+
[--control-plane <url>]
|
|
113
|
+
Print the id for this piece of work, and the one line to write into the
|
|
114
|
+
commit it produces. Pass the id to every promise read you make, so a check
|
|
115
|
+
that goes red later can be read back against what Balladeer told you
|
|
116
|
+
before you started. --new starts a different session; --record reads the
|
|
117
|
+
Balladeer-Session line out of the commit at HEAD and tells Balladeer which
|
|
118
|
+
commit this session wrote. Your commit message never leaves this machine:
|
|
119
|
+
only the session id and the commit SHA are sent.
|
|
120
|
+
|
|
121
|
+
${CLI_INVOCATION} explain [--control-plane <url>]
|
|
83
122
|
Print, word for word, what Balladeer can and cannot see, where to watch
|
|
84
123
|
your promises, and what leaving costs.
|
|
85
124
|
|
|
86
|
-
|
|
125
|
+
${CLI_INVOCATION} whoami [--json] [--control-plane <url>]
|
|
87
126
|
Report the stored setup session's workspace, role, scopes, and expiry.
|
|
88
127
|
|
|
89
|
-
|
|
128
|
+
${CLI_INVOCATION} agent rotate [--control-plane <url>]
|
|
90
129
|
Replace this repository's agent credential. A setup session cannot do this,
|
|
91
130
|
because rotating revokes the connections the repository already has.
|
|
92
131
|
|
|
93
|
-
|
|
132
|
+
${CLI_INVOCATION} mcp [--repository <uuid>] [--control-plane <url>]
|
|
94
133
|
Forward one MCP session over stdio using this repository's agent connection.
|
|
95
134
|
|
|
96
135
|
Exit codes: 0 progress reported truthfully, 2 pairing expired or denied or already
|
|
97
136
|
claimed, 3 this copy is too old for the server, 4 usage or credential store problem,
|
|
98
|
-
5 transport or server failure
|
|
137
|
+
5 transport or server failure, 6 an earlier Balladeer is still installed on this
|
|
138
|
+
machine and nothing was changed.
|
|
99
139
|
`;
|
|
100
140
|
const SUBCOMMANDS = { agent: ["rotate"] };
|
|
101
141
|
export function parseArguments(argv) {
|
|
@@ -111,7 +151,13 @@ export function parseArguments(argv) {
|
|
|
111
151
|
let json = false;
|
|
112
152
|
let wait = false;
|
|
113
153
|
let refresh = false;
|
|
154
|
+
let force = false;
|
|
155
|
+
let existingOnly = false;
|
|
156
|
+
let claudeDesktop;
|
|
114
157
|
let installHook = false;
|
|
158
|
+
let fresh = false;
|
|
159
|
+
let record = false;
|
|
160
|
+
let again = false;
|
|
115
161
|
let runner;
|
|
116
162
|
let controlPlane;
|
|
117
163
|
let repo;
|
|
@@ -147,14 +193,28 @@ export function parseArguments(argv) {
|
|
|
147
193
|
const inline = rest.length > 0 ? rest.join("=") : undefined;
|
|
148
194
|
if (name === "--json")
|
|
149
195
|
json = true;
|
|
196
|
+
else if (name === "--again")
|
|
197
|
+
again = true;
|
|
150
198
|
else if (name === "--install-hook")
|
|
151
199
|
installHook = true;
|
|
200
|
+
else if (name === "--new")
|
|
201
|
+
fresh = true;
|
|
202
|
+
else if (name === "--record")
|
|
203
|
+
record = true;
|
|
152
204
|
else if (name === "--runner")
|
|
153
205
|
runner = value("--runner", inline);
|
|
154
206
|
else if (name === "--wait")
|
|
155
207
|
wait = true;
|
|
156
208
|
else if (name === "--refresh")
|
|
157
209
|
refresh = true;
|
|
210
|
+
else if (name === "--force")
|
|
211
|
+
force = true;
|
|
212
|
+
else if (name === "--existing")
|
|
213
|
+
existingOnly = true;
|
|
214
|
+
else if (name === "--claude-desktop")
|
|
215
|
+
claudeDesktop = true;
|
|
216
|
+
else if (name === "--no-claude-desktop")
|
|
217
|
+
claudeDesktop = false;
|
|
158
218
|
else if (name === "--control-plane")
|
|
159
219
|
controlPlane = value("--control-plane", inline);
|
|
160
220
|
else if (name === "--repo")
|
|
@@ -181,8 +241,20 @@ export function parseArguments(argv) {
|
|
|
181
241
|
// value that lost its flag, and it was refused before this loop learned to
|
|
182
242
|
// collect them, so it is refused here rather than ignored.
|
|
183
243
|
// `affected` is the other one, and the words it takes are paths in the change
|
|
184
|
-
// in front of the person.
|
|
185
|
-
|
|
244
|
+
// in front of the person. `prepare` and `status` each take exactly one bare
|
|
245
|
+
// word, the promise id a person copied off a promise page, which is the form
|
|
246
|
+
// both of those commands are told to people and to agents in.
|
|
247
|
+
// `--again` invalidates a packet somebody may be halfway through using, so it
|
|
248
|
+
// belongs to the one command that mints one. Accepted silently elsewhere, it
|
|
249
|
+
// would read as a general "do it anyway" flag.
|
|
250
|
+
if (again && command !== "prepare") {
|
|
251
|
+
throw new StoreError("usage", `--again belongs to prepare: run \`${CLI_INVOCATION} prepare <promise id> --again\`.`);
|
|
252
|
+
}
|
|
253
|
+
if (positional.length > 0 &&
|
|
254
|
+
command !== "invite" &&
|
|
255
|
+
command !== "affected" &&
|
|
256
|
+
command !== "prepare" &&
|
|
257
|
+
command !== "status") {
|
|
186
258
|
throw new StoreError("usage", `${command} takes no bare arguments: ${positional.join(", ")}.`);
|
|
187
259
|
}
|
|
188
260
|
// `--repository` names one repository everywhere but `setup`, where it names
|
|
@@ -193,7 +265,28 @@ export function parseArguments(argv) {
|
|
|
193
265
|
// nothing else. Accepting it silently elsewhere would let somebody run
|
|
194
266
|
// `status --refresh`, see no error, and believe their install was repaired.
|
|
195
267
|
if (refresh && command !== "setup") {
|
|
196
|
-
throw new StoreError("usage",
|
|
268
|
+
throw new StoreError("usage", `--refresh belongs to setup: run \`${CLI_INVOCATION} setup --refresh\`.`);
|
|
269
|
+
}
|
|
270
|
+
// `--force` says one thing only: set up alongside an earlier Balladeer. Taken
|
|
271
|
+
// silently by another command it would read as a general "do it anyway" flag,
|
|
272
|
+
// which is exactly what nothing else here offers.
|
|
273
|
+
if (force && command !== "setup") {
|
|
274
|
+
throw new StoreError("usage", `--force belongs to setup: run \`${CLI_INVOCATION} setup --force\`.`);
|
|
275
|
+
}
|
|
276
|
+
if (existingOnly && command !== "setup") {
|
|
277
|
+
throw new StoreError("usage", `--existing belongs to setup: run \`${CLI_INVOCATION} setup --existing\`.`);
|
|
278
|
+
}
|
|
279
|
+
if (existingOnly && createWorkspace !== undefined) {
|
|
280
|
+
throw new StoreError("usage", "--existing cannot be combined with --create-workspace: one refuses repository enrollment and the other starts a new workspace.");
|
|
281
|
+
}
|
|
282
|
+
if (existingOnly && repositories.length > 1) {
|
|
283
|
+
throw new StoreError("usage", "--existing connects one checkout and never adds repositories. Leave off --repository, or name only this checkout.");
|
|
284
|
+
}
|
|
285
|
+
// Connecting a chat client is something setup does, so the flag belongs to
|
|
286
|
+
// setup. Accepted silently on `status`, it would let somebody believe they had
|
|
287
|
+
// connected Claude desktop when nothing had written a line.
|
|
288
|
+
if (claudeDesktop !== undefined && command !== "setup") {
|
|
289
|
+
throw new StoreError("usage", `--claude-desktop belongs to setup: run \`${CLI_INVOCATION} setup --claude-desktop\`.`);
|
|
197
290
|
}
|
|
198
291
|
if (command !== "setup" && repositories.length > 1) {
|
|
199
292
|
throw new StoreError("usage", `${command} acts on one repository. Name --repository once, or run it again for the other.`);
|
|
@@ -205,12 +298,18 @@ export function parseArguments(argv) {
|
|
|
205
298
|
json,
|
|
206
299
|
wait,
|
|
207
300
|
refresh,
|
|
301
|
+
force,
|
|
302
|
+
existingOnly,
|
|
303
|
+
claudeDesktop,
|
|
208
304
|
repo,
|
|
209
305
|
file,
|
|
210
306
|
repository: repositories[0],
|
|
211
307
|
repositories,
|
|
212
308
|
owner,
|
|
213
309
|
installHook,
|
|
310
|
+
fresh,
|
|
311
|
+
record,
|
|
312
|
+
again,
|
|
214
313
|
runner,
|
|
215
314
|
createWorkspace,
|
|
216
315
|
positional,
|
|
@@ -248,6 +347,9 @@ async function dispatch(parsed, write) {
|
|
|
248
347
|
json: parsed.json,
|
|
249
348
|
wait: parsed.wait,
|
|
250
349
|
refresh: parsed.refresh,
|
|
350
|
+
force: parsed.force,
|
|
351
|
+
existingOnly: parsed.existingOnly,
|
|
352
|
+
...(parsed.claudeDesktop === undefined ? {} : { claudeDesktop: parsed.claudeDesktop }),
|
|
251
353
|
...(parsed.repo === undefined ? {} : { repo: parsed.repo }),
|
|
252
354
|
repositories: parsed.repositories,
|
|
253
355
|
...(parsed.createWorkspace === undefined
|
|
@@ -296,6 +398,18 @@ async function dispatch(parsed, write) {
|
|
|
296
398
|
cwd: process.cwd(),
|
|
297
399
|
write,
|
|
298
400
|
});
|
|
401
|
+
case "session":
|
|
402
|
+
return runSession({
|
|
403
|
+
controlPlane: parsed.controlPlane,
|
|
404
|
+
json: parsed.json,
|
|
405
|
+
record: parsed.record,
|
|
406
|
+
fresh: parsed.fresh,
|
|
407
|
+
...(parsed.repo === undefined ? {} : { repo: parsed.repo }),
|
|
408
|
+
...(parsed.repository === undefined ? {} : { repository: parsed.repository }),
|
|
409
|
+
environment: process.env,
|
|
410
|
+
cwd: process.cwd(),
|
|
411
|
+
write,
|
|
412
|
+
});
|
|
299
413
|
case "affected":
|
|
300
414
|
return runAffected({
|
|
301
415
|
json: parsed.json,
|
|
@@ -322,6 +436,26 @@ async function dispatch(parsed, write) {
|
|
|
322
436
|
write,
|
|
323
437
|
});
|
|
324
438
|
}
|
|
439
|
+
case "prepare": {
|
|
440
|
+
// One id, and one only. Preparing two packets from one line would mint
|
|
441
|
+
// two one-time identities and write two files, and reporting the first
|
|
442
|
+
// silently would leave the second promise unprepared with nobody told.
|
|
443
|
+
if (parsed.positional.length !== 1) {
|
|
444
|
+
process.stderr.write(`prepare reads one promise id: ${CLI_INVOCATION} prepare <promise id> [--again].\n`);
|
|
445
|
+
return 4;
|
|
446
|
+
}
|
|
447
|
+
return runPrepare({
|
|
448
|
+
controlPlane: parsed.controlPlane,
|
|
449
|
+
json: parsed.json,
|
|
450
|
+
promiseId: parsed.positional[0],
|
|
451
|
+
again: parsed.again,
|
|
452
|
+
...(parsed.repo === undefined ? {} : { repo: parsed.repo }),
|
|
453
|
+
...(parsed.repository === undefined ? {} : { repository: parsed.repository }),
|
|
454
|
+
environment: process.env,
|
|
455
|
+
cwd: process.cwd(),
|
|
456
|
+
write,
|
|
457
|
+
});
|
|
458
|
+
}
|
|
325
459
|
case "propose":
|
|
326
460
|
return runPropose({
|
|
327
461
|
controlPlane: parsed.controlPlane,
|
|
@@ -384,9 +518,32 @@ async function dispatch(parsed, write) {
|
|
|
384
518
|
return 4;
|
|
385
519
|
}
|
|
386
520
|
}
|
|
521
|
+
/**
|
|
522
|
+
* The real file behind a path, following symlinks.
|
|
523
|
+
*
|
|
524
|
+
* `npx` and every global install run this program through a
|
|
525
|
+
* `node_modules/.bin/balladeer` symlink, so `process.argv[1]` is the link and
|
|
526
|
+
* not the file Node loaded. `resolve` does not follow a link, so comparing
|
|
527
|
+
* resolved paths made the guard below false for every customer: the process
|
|
528
|
+
* exited 0 having printed nothing, and only a checkout running
|
|
529
|
+
* `node dist/cli.js` ever saw the program run at all.
|
|
530
|
+
*
|
|
531
|
+
* A path that cannot be read back falls through to the resolved form rather
|
|
532
|
+
* than throwing, because a guard that decides whether the program runs must
|
|
533
|
+
* never be the thing that stops it.
|
|
534
|
+
*/
|
|
535
|
+
function programPath(path) {
|
|
536
|
+
const absolute = resolve(path);
|
|
537
|
+
try {
|
|
538
|
+
return realpathSync(absolute);
|
|
539
|
+
}
|
|
540
|
+
catch {
|
|
541
|
+
return absolute;
|
|
542
|
+
}
|
|
543
|
+
}
|
|
387
544
|
// Only when this file is the program, so a test may import `main` without the
|
|
388
545
|
// import itself running a command.
|
|
389
546
|
const entry = process.argv[1];
|
|
390
|
-
if (entry !== undefined && fileURLToPath(import.meta.url) ===
|
|
547
|
+
if (entry !== undefined && programPath(fileURLToPath(import.meta.url)) === programPath(entry)) {
|
|
391
548
|
process.exitCode = await main(process.argv.slice(2));
|
|
392
549
|
}
|
package/dist/client.d.ts
CHANGED
|
@@ -18,7 +18,7 @@ export declare class RefusalError extends Error {
|
|
|
18
18
|
*
|
|
19
19
|
* Behind a platform proxy a server can resolve a link against the machine it is
|
|
20
20
|
* running on rather than against the address customers use, and the first
|
|
21
|
-
* production `propose` printed exactly that: `
|
|
21
|
+
* production `propose` printed exactly that: a review link on `localhost:8080`
|
|
22
22
|
* as the place to go and agree. A command that prints such a link is worse than
|
|
23
23
|
* one that prints none, because the person tries it.
|
|
24
24
|
*
|
|
@@ -26,7 +26,29 @@ export declare class RefusalError extends Error {
|
|
|
26
26
|
* identifier and this builds the address, which is why there is no parameter
|
|
27
27
|
* here for a server's own link to arrive through.
|
|
28
28
|
*/
|
|
29
|
-
export declare function reviewLink(controlPlane: string, path: string): string;
|
|
29
|
+
export declare function reviewLink(controlPlane: string, path: string, workspaceId?: string): string;
|
|
30
|
+
/**
|
|
31
|
+
* The one place this command decides where a person reads a proposal.
|
|
32
|
+
*
|
|
33
|
+
* Every review address this command prints, in a receipt, in a discovery run,
|
|
34
|
+
* or in its JSON, is built here, so the address is one edit rather than a hunt
|
|
35
|
+
* and a printed link cannot fall behind the page it names. It matches the
|
|
36
|
+
* server's own `proposal-links` helper: the page is `/proposals`, and
|
|
37
|
+
* the retired address redirects to it permanently, so a link an older copy of
|
|
38
|
+
* this command already printed still lands on it.
|
|
39
|
+
*/
|
|
40
|
+
export declare function proposalReviewLink(controlPlane: string, proposalId: string, workspaceId?: string): string;
|
|
41
|
+
/** The address the whole waiting inbox is read on. */
|
|
42
|
+
export declare function proposalInboxLink(controlPlane: string): string;
|
|
43
|
+
/**
|
|
44
|
+
* One link that opens exactly the proposals named, and nothing else.
|
|
45
|
+
*
|
|
46
|
+
* The ids travel in the query string because the batch is exactly these
|
|
47
|
+
* promises: a link to the whole inbox would also open whatever was already
|
|
48
|
+
* waiting there, and a filter by repository would open a different set
|
|
49
|
+
* tomorrow.
|
|
50
|
+
*/
|
|
51
|
+
export declare function batchProposalReviewLink(controlPlane: string, proposalIds: readonly string[]): string;
|
|
30
52
|
export type RequestOptions = Readonly<{
|
|
31
53
|
method: "GET" | "POST";
|
|
32
54
|
path: string;
|