balladeer 0.0.5 → 1.0.1

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.
Files changed (72) hide show
  1. package/LICENSE +200 -5
  2. package/README.md +167 -68
  3. package/dist/agent.d.ts +126 -0
  4. package/dist/agent.js +209 -0
  5. package/dist/cli.d.ts +48 -0
  6. package/dist/cli.js +531 -0
  7. package/dist/client.d.ts +66 -0
  8. package/dist/client.js +142 -0
  9. package/dist/commands/affected.d.ts +22 -0
  10. package/dist/commands/affected.js +123 -0
  11. package/dist/commands/check-seals.d.ts +37 -0
  12. package/dist/commands/check-seals.js +289 -0
  13. package/dist/commands/discover.d.ts +68 -0
  14. package/dist/commands/discover.js +403 -0
  15. package/dist/commands/explain.d.ts +35 -0
  16. package/dist/commands/explain.js +90 -0
  17. package/dist/commands/invite.d.ts +24 -0
  18. package/dist/commands/invite.js +198 -0
  19. package/dist/commands/mcp.d.ts +65 -0
  20. package/dist/commands/mcp.js +202 -0
  21. package/dist/commands/prepare.d.ts +74 -0
  22. package/dist/commands/prepare.js +217 -0
  23. package/dist/commands/propose.d.ts +69 -0
  24. package/dist/commands/propose.js +284 -0
  25. package/dist/commands/repositories.d.ts +18 -0
  26. package/dist/commands/repositories.js +185 -0
  27. package/dist/commands/session.d.ts +35 -0
  28. package/dist/commands/session.js +118 -0
  29. package/dist/commands/setup.d.ts +98 -0
  30. package/dist/commands/setup.js +1600 -0
  31. package/dist/commands/status.d.ts +51 -0
  32. package/dist/commands/status.js +542 -0
  33. package/dist/commands/touch-map.d.ts +42 -0
  34. package/dist/commands/touch-map.js +251 -0
  35. package/dist/commands/whoami.d.ts +8 -0
  36. package/dist/commands/whoami.js +80 -0
  37. package/dist/conventions.d.ts +77 -0
  38. package/dist/conventions.js +183 -0
  39. package/dist/copy.d.ts +224 -0
  40. package/dist/copy.js +641 -0
  41. package/dist/currency.d.ts +31 -0
  42. package/dist/currency.js +72 -0
  43. package/dist/desktop-config.d.ts +85 -0
  44. package/dist/desktop-config.js +217 -0
  45. package/dist/gh.d.ts +80 -0
  46. package/dist/gh.js +188 -0
  47. package/dist/git.d.ts +91 -0
  48. package/dist/git.js +226 -0
  49. package/dist/legacy.d.ts +41 -0
  50. package/dist/legacy.js +143 -0
  51. package/dist/local-time.d.ts +66 -0
  52. package/dist/local-time.js +84 -0
  53. package/dist/markers.d.ts +76 -0
  54. package/dist/markers.js +125 -0
  55. package/dist/mcp-config.d.ts +109 -0
  56. package/dist/mcp-config.js +234 -0
  57. package/dist/release.d.ts +55 -0
  58. package/dist/release.js +67 -0
  59. package/dist/repository.d.ts +8 -0
  60. package/dist/repository.js +32 -0
  61. package/dist/seals.d.ts +48 -0
  62. package/dist/seals.js +112 -0
  63. package/dist/session.d.ts +84 -0
  64. package/dist/session.js +135 -0
  65. package/dist/store.d.ts +108 -0
  66. package/dist/store.js +237 -0
  67. package/dist/touch-map.d.ts +241 -0
  68. package/dist/touch-map.js +487 -0
  69. package/dist/wire.d.ts +674 -0
  70. package/dist/wire.js +20 -0
  71. package/package.json +19 -10
  72. package/bin/balladeer.js +0 -161
@@ -0,0 +1,198 @@
1
+ import { ClientTooOldError, RefusalError, TransportError, request } from "../client.js";
2
+ import { formatInstant } from "../local-time.js";
3
+ import { commandLine } from "../release.js";
4
+ import { StoreError, findSession, readCredentials } from "../store.js";
5
+ import {} from "../wire.js";
6
+ /**
7
+ * What a person types, and what the ledger stores.
8
+ *
9
+ * "Administrator" is the word every screen uses and the word a person says out
10
+ * loud, and `admin` is the value the ledger holds. Both are accepted so nobody
11
+ * has to know which of the two this command wanted, and the confirmation prints
12
+ * the long form back, because that is the word on the settings page they will
13
+ * see this person listed under.
14
+ */
15
+ const ROLES = {
16
+ administrator: "admin",
17
+ admin: "admin",
18
+ contributor: "contributor",
19
+ viewer: "viewer",
20
+ };
21
+ export const ROLE_NAMES = {
22
+ admin: "administrator",
23
+ contributor: "contributor",
24
+ viewer: "viewer",
25
+ };
26
+ /** What each role may do, in one clause, so nobody invites by guessing. */
27
+ export const ROLE_MEANINGS = {
28
+ admin: "read this workspace, propose promises, change setup, and invite people",
29
+ contributor: "read this workspace and propose promises",
30
+ viewer: "read this workspace",
31
+ };
32
+ export function parseRole(value) {
33
+ if (value === undefined)
34
+ return "contributor";
35
+ return ROLES[value.trim().toLowerCase()];
36
+ }
37
+ /**
38
+ * An address this command will send. Deliberately narrow rather than clever: it
39
+ * refuses here so a typo costs a sentence rather than an invitation nobody can
40
+ * recall, and the server's own schema refuses it again.
41
+ */
42
+ const EMAIL = /^[^\s@,;]+@[^\s@,;.]+(?:\.[^\s@,;.]+)+$/;
43
+ export function looksLikeEmail(value) {
44
+ return value.length <= 320 && EMAIL.test(value);
45
+ }
46
+ function emit(options, step) {
47
+ if (options.json)
48
+ options.write(`${JSON.stringify(step)}\n`);
49
+ }
50
+ function say(options, text) {
51
+ if (!options.json)
52
+ options.write(`${text}\n`);
53
+ }
54
+ function fail(options, reason, message, exitCode) {
55
+ say(options, message);
56
+ emit(options, { step: "error", reason, message, changed: false, exitCode });
57
+ return exitCode;
58
+ }
59
+ /**
60
+ * The refusal a person meets when their approver's role never carried this.
61
+ *
62
+ * Said before anything is sent, from the scopes the session itself reports,
63
+ * because the alternative is a person typing four addresses and being refused
64
+ * by the server four times for a reason that was knowable before the first one.
65
+ */
66
+ function withoutTheScope(options, session) {
67
+ return fail(options, "invite_not_permitted", [
68
+ `This setup session cannot invite anybody into ${session.workspaceName}.`,
69
+ " Inviting is an administrator's act, and a session carries only what the person who approved it could do unaided.",
70
+ ` Next: ask an administrator of ${session.workspaceName} to run this command, or to invite people in Balladeer under workspace settings.`,
71
+ ].join("\n"), 4);
72
+ }
73
+ /**
74
+ * Invites teammates by email, one at a time, and says what happened to each.
75
+ *
76
+ * Per address rather than per run. Sending four invitations is four separate
77
+ * things that can each succeed or be refused on their own, and a person reading
78
+ * "3 of 4 sent" cannot tell whose email is missing. Every address gets its own
79
+ * line and its own object, and the exit code says only whether every one of
80
+ * them went.
81
+ */
82
+ export async function runInvite(options) {
83
+ if (options.emails.length === 0) {
84
+ return fail(options, "usage", `Name at least one email address to invite. For example: ${commandLine(null, "invite alice@example.com")}`, 4);
85
+ }
86
+ const malformed = options.emails.filter((email) => !looksLikeEmail(email));
87
+ if (malformed.length > 0) {
88
+ return fail(options, "usage", `That is not an email address: ${malformed.join(", ")}. Nothing was sent.`, 4);
89
+ }
90
+ let credentials;
91
+ try {
92
+ credentials = readCredentials(options.environment);
93
+ }
94
+ catch (error) {
95
+ return fail(options, error instanceof StoreError ? error.code : "credential_store_unusable", error instanceof StoreError ? error.message : String(error), 4);
96
+ }
97
+ const session = findSession(credentials, options.controlPlane);
98
+ if (session === undefined) {
99
+ return fail(options, "no_stored_session", `No Balladeer session is stored for ${options.controlPlane}. Run: ${commandLine(null, "setup")}`, 4);
100
+ }
101
+ // Read off the stored session rather than attempted and reported back. The
102
+ // approval page lists what the person granted, and this is one of the five.
103
+ if (!session.scopes.includes("workspace:invite")) {
104
+ return withoutTheScope(options, session);
105
+ }
106
+ let refused = 0;
107
+ for (const email of options.emails) {
108
+ const sent = await inviteOne(options, session, email);
109
+ if (!sent)
110
+ refused += 1;
111
+ }
112
+ if (refused === 0) {
113
+ say(options, `\nEach person gets an email from Balladeer with a link to accept. Nobody is in ${session.workspaceName} until they accept, and until then they are listed as invited under workspace settings.`);
114
+ return 0;
115
+ }
116
+ return 5;
117
+ }
118
+ async function inviteOne(options, session, email) {
119
+ const body = { email, requestedRole: options.role };
120
+ try {
121
+ const answer = await request(options.controlPlane, {
122
+ method: "POST",
123
+ path: "/api/setup/v1/invitations",
124
+ bearer: session.token,
125
+ body,
126
+ });
127
+ const role = ROLE_NAMES[answer.requestedRole];
128
+ say(options, `Invited ${answer.email} to ${answer.workspaceName} as ${role}. Balladeer has sent them the email.`);
129
+ say(options, ` They can ${ROLE_MEANINGS[answer.requestedRole]}. They join when they accept, and not before.`);
130
+ if (answer.expiresAt !== null) {
131
+ say(options, ` The invitation stops working at ${formatInstant(answer.expiresAt)}.`);
132
+ }
133
+ emit(options, {
134
+ step: "invitation",
135
+ status: "sent",
136
+ email: answer.email,
137
+ role: answer.requestedRole,
138
+ workspace: answer.workspaceName,
139
+ expiresAt: answer.expiresAt,
140
+ });
141
+ return true;
142
+ }
143
+ catch (error) {
144
+ const { reason, message } = invitationRefusal(options, email, error);
145
+ say(options, message);
146
+ emit(options, {
147
+ step: "invitation",
148
+ status: "refused",
149
+ email,
150
+ role: options.role,
151
+ reason,
152
+ message,
153
+ });
154
+ return false;
155
+ }
156
+ }
157
+ /**
158
+ * One refusal, in the words the person can act on.
159
+ *
160
+ * Every arm names the address, because a run inviting four people prints four
161
+ * lines and a bare "already a member" belongs to one of them. Nothing here
162
+ * invents a remedy the server did not offer: where the server said why, that
163
+ * sentence is what is printed.
164
+ */
165
+ function invitationRefusal(options, email, error) {
166
+ if (error instanceof ClientTooOldError) {
167
+ return { reason: "client_too_old", message: `${email}: ${error.message}` };
168
+ }
169
+ if (error instanceof TransportError) {
170
+ return {
171
+ reason: "control_plane_unreachable",
172
+ message: `${email}: Balladeer could not be reached at ${options.controlPlane}: ${error.message}. No invitation was sent to this address.`,
173
+ };
174
+ }
175
+ if (error instanceof RefusalError) {
176
+ if (error.code === "delegated_authority_refused" || error.code === "action_not_allowed") {
177
+ return {
178
+ reason: error.code,
179
+ message: `${email}: this setup session may not invite anybody. Ask an administrator to run this command, or to invite people in Balladeer under workspace settings.`,
180
+ };
181
+ }
182
+ if (error.code === "session_expired" || error.code === "session_revoked") {
183
+ return {
184
+ reason: error.code,
185
+ message: `${email}: this setup session has ended, so nothing was sent. Run \`${commandLine(null, "setup")}\` to pair again.`,
186
+ };
187
+ }
188
+ // The server's own sentence. "That email already has an active membership"
189
+ // and "an incompatible or already-confirmed invitation is still open for
190
+ // that email" are both things a person acts on, and both are already
191
+ // written the way a person reads them.
192
+ return { reason: error.code, message: `${email}: ${error.message}` };
193
+ }
194
+ return {
195
+ reason: "invitation_failed",
196
+ message: `${email}: Balladeer could not send this invitation. Nothing was sent to this address.`,
197
+ };
198
+ }
@@ -0,0 +1,65 @@
1
+ import { type StoredAgent } from "../store.js";
2
+ export { forwarderHeaders, selectAgent, type AgentSelection } from "../agent.js";
3
+ export type McpOptions = Readonly<{
4
+ controlPlane: string;
5
+ /**
6
+ * Which repository's credential to forward. The host config setup writes
7
+ * always names it, because a workspace holds several repositories and each one
8
+ * has its own connection. Without it, the credential belonging to the
9
+ * repository this process was started in is used, and where there is no such
10
+ * credential the command refuses rather than guessing.
11
+ */
12
+ repositoryId?: string;
13
+ environment: NodeJS.ProcessEnv;
14
+ cwd: string;
15
+ stdin: NodeJS.ReadableStream;
16
+ write: (text: string) => void;
17
+ error: (text: string) => void;
18
+ }>;
19
+ export type ProbeResult = Readonly<{
20
+ kind: "answered";
21
+ repositoryId: string;
22
+ }> | Readonly<{
23
+ kind: "refused";
24
+ reason: string;
25
+ nextAction: string;
26
+ }>;
27
+ /**
28
+ * One real call over the connection just issued, before anything claims it
29
+ * works.
30
+ *
31
+ * It is `get_promise_setup` because that tool answers for the bound repository
32
+ * and nothing else, so its answer proves three things at once: the credential
33
+ * authenticated on the MCP endpoint, Balladeer served this repository's
34
+ * connection rather than another's, and the frames this forwarder writes are
35
+ * frames that server accepts. A response for a different repository is a failure
36
+ * here, not a curiosity.
37
+ *
38
+ * It sends what the forwarder sends, over the endpoint the forwarder would use,
39
+ * and refuses the same entries the forwarder refuses. What it cannot prove is
40
+ * whether an agent host has loaded the entry yet, which is why the step that
41
+ * calls this also names the host's own next action rather than implying there
42
+ * is none.
43
+ */
44
+ export declare function probeAgentConnection(agent: StoredAgent, timeoutMs?: number): Promise<ProbeResult>;
45
+ /**
46
+ * The frame a host is owed when the server refuses this copy of the program.
47
+ *
48
+ * A 426 body is the control plane's JSON, not JSON-RPC, and relaying it to a
49
+ * host would surface as a protocol error with no sentence in it. The host is
50
+ * told in its own protocol what happened and which line fixes it, and the same
51
+ * sentence goes to stderr for whoever is reading the log.
52
+ */
53
+ export declare function staleClientFrame(id: unknown, update: string): string;
54
+ /**
55
+ * The frame a host is owed when the credential itself is finished.
56
+ *
57
+ * The server refuses a revoked connection at the door, so the model never
58
+ * reaches a tool and never sees the tool-level refusal that would have told it
59
+ * apart from a missing scope. Without this it sees a server that died. The
60
+ * sentence is the same distinction the server's own refusals draw: this is the
61
+ * connection being gone, not this tool being unavailable, and the remedy is a
62
+ * person's.
63
+ */
64
+ export declare function revokedConnectionFrame(id: unknown, controlPlane: string): string;
65
+ export declare function runMcp(options: McpOptions): Promise<number>;
@@ -0,0 +1,202 @@
1
+ import { createInterface } from "node:readline";
2
+ import { callAgentTool, forwarderHeaders, noAgentCredentialSentence, safeJson, selectAgent, structuredString, updateLine, } from "../agent.js";
3
+ import { noteServerVersion, updateNotice } from "../currency.js";
4
+ import { repositoryHint } from "../repository.js";
5
+ import { StoreError, readCredentials } from "../store.js";
6
+ // The three commands that use an agent connection select and address it through
7
+ // one module. These are re-exported because this is where the forwarder's
8
+ // callers already look for them.
9
+ export { forwarderHeaders, selectAgent } from "../agent.js";
10
+ const MAX_REQUEST_BYTES = 1024 * 1024;
11
+ const MAX_RESPONSE_BYTES = 4 * 1024 * 1024;
12
+ /**
13
+ * One real call over the connection just issued, before anything claims it
14
+ * works.
15
+ *
16
+ * It is `get_promise_setup` because that tool answers for the bound repository
17
+ * and nothing else, so its answer proves three things at once: the credential
18
+ * authenticated on the MCP endpoint, Balladeer served this repository's
19
+ * connection rather than another's, and the frames this forwarder writes are
20
+ * frames that server accepts. A response for a different repository is a failure
21
+ * here, not a curiosity.
22
+ *
23
+ * It sends what the forwarder sends, over the endpoint the forwarder would use,
24
+ * and refuses the same entries the forwarder refuses. What it cannot prove is
25
+ * whether an agent host has loaded the entry yet, which is why the step that
26
+ * calls this also names the host's own next action rather than implying there
27
+ * is none.
28
+ */
29
+ export async function probeAgentConnection(agent, timeoutMs = 15_000) {
30
+ const call = await callAgentTool(agent, "get_promise_setup", {}, timeoutMs);
31
+ switch (call.kind) {
32
+ case "endpoint_refused":
33
+ return {
34
+ kind: "refused",
35
+ reason: call.reason,
36
+ nextAction: "Issue the connection again from the Balladeer setup page.",
37
+ };
38
+ case "unreachable":
39
+ return {
40
+ kind: "refused",
41
+ reason: "Balladeer's MCP endpoint could not be reached from this machine",
42
+ nextAction: "Check this machine's network access, then run this command again.",
43
+ };
44
+ case "client_too_old":
45
+ return {
46
+ kind: "refused",
47
+ reason: "this copy of the command is too old for that Balladeer",
48
+ nextAction: `Update it with: ${call.update}, then run this command again.`,
49
+ };
50
+ case "unauthorized":
51
+ return {
52
+ kind: "refused",
53
+ reason: "Balladeer refused the credential this step just issued",
54
+ nextAction: "Issue the connection again from the Balladeer setup page.",
55
+ };
56
+ case "http":
57
+ return {
58
+ kind: "refused",
59
+ reason: `Balladeer answered ${call.status} to a first call over this connection`,
60
+ nextAction: "Run this command again; if it repeats, report the status above.",
61
+ };
62
+ case "result": {
63
+ const repositoryId = structuredString(call.structured, "repositoryId");
64
+ if (repositoryId === undefined)
65
+ break;
66
+ if (repositoryId !== agent.repositoryId) {
67
+ return {
68
+ kind: "refused",
69
+ reason: "that connection answered for a different repository",
70
+ nextAction: "Issue the connection again from the Balladeer setup page, and do not use it.",
71
+ };
72
+ }
73
+ return { kind: "answered", repositoryId };
74
+ }
75
+ default:
76
+ break;
77
+ }
78
+ return {
79
+ kind: "refused",
80
+ reason: "Balladeer's answer to a first call over this connection was not a tool result",
81
+ nextAction: "Run this command again; if it repeats, report that to Balladeer.",
82
+ };
83
+ }
84
+ /**
85
+ * The frame a host is owed when the server refuses this copy of the program.
86
+ *
87
+ * A 426 body is the control plane's JSON, not JSON-RPC, and relaying it to a
88
+ * host would surface as a protocol error with no sentence in it. The host is
89
+ * told in its own protocol what happened and which line fixes it, and the same
90
+ * sentence goes to stderr for whoever is reading the log.
91
+ */
92
+ export function staleClientFrame(id, update) {
93
+ return errorFrame(id, `This copy of the Balladeer command is too old for this Balladeer. Update it with: ${update}`);
94
+ }
95
+ /**
96
+ * The frame a host is owed when the credential itself is finished.
97
+ *
98
+ * The server refuses a revoked connection at the door, so the model never
99
+ * reaches a tool and never sees the tool-level refusal that would have told it
100
+ * apart from a missing scope. Without this it sees a server that died. The
101
+ * sentence is the same distinction the server's own refusals draw: this is the
102
+ * connection being gone, not this tool being unavailable, and the remedy is a
103
+ * person's.
104
+ */
105
+ export function revokedConnectionFrame(id, controlPlane) {
106
+ return errorFrame(id, `Balladeer refused this agent connection: it was revoked, or the repository it was bound to is no longer enrolled. This is the connection being gone rather than one tool being unavailable, so retrying will not help. Ask the person to run Balladeer setup again in this repository, or to issue a new connection at ${controlPlane}/setup.`);
107
+ }
108
+ function errorFrame(id, message) {
109
+ return JSON.stringify({
110
+ jsonrpc: "2.0",
111
+ id: id ?? null,
112
+ error: { code: -32000, message },
113
+ });
114
+ }
115
+ function frameId(line) {
116
+ try {
117
+ const parsed = JSON.parse(line);
118
+ if (parsed !== null && typeof parsed === "object") {
119
+ const id = parsed.id;
120
+ if (typeof id === "string" || typeof id === "number")
121
+ return id;
122
+ }
123
+ }
124
+ catch {
125
+ // A frame we cannot parse still gets an answer, with a null id.
126
+ }
127
+ return null;
128
+ }
129
+ export async function runMcp(options) {
130
+ let credentials;
131
+ try {
132
+ credentials = readCredentials(options.environment);
133
+ }
134
+ catch (error) {
135
+ options.error(`${error instanceof StoreError ? error.message : String(error)}\n`);
136
+ return 4;
137
+ }
138
+ const selection = selectAgent(credentials.agents, options.controlPlane, options.repositoryId, repositoryHint(options.cwd));
139
+ if (selection.kind === "refused") {
140
+ // Issuing one in the browser is what this used to suggest here, and it is the
141
+ // move that produces this state: the bearer is shown once, there, and the
142
+ // forwarder reads the store on this machine.
143
+ options.error(selection.missingFor === undefined
144
+ ? `${selection.reason} Run setup in this repository, or issue the connection at ${options.controlPlane}/setup.\n`
145
+ : `${noAgentCredentialSentence(selection.missingFor)}\n`);
146
+ return 4;
147
+ }
148
+ const { agent } = selection;
149
+ let saidUpdate = false;
150
+ const lines = createInterface({ input: options.stdin, crlfDelay: Infinity });
151
+ for await (const line of lines) {
152
+ if (line.trim().length === 0)
153
+ continue;
154
+ if (Buffer.byteLength(line, "utf8") > MAX_REQUEST_BYTES) {
155
+ options.error("Balladeer refused a request frame larger than 1 MiB.\n");
156
+ return 5;
157
+ }
158
+ let response;
159
+ try {
160
+ response = await fetch(agent.mcpUrl, {
161
+ method: "POST",
162
+ headers: forwarderHeaders(agent.token),
163
+ body: line,
164
+ redirect: "manual",
165
+ });
166
+ }
167
+ catch {
168
+ options.error("Balladeer could not be reached. Nothing was changed.\n");
169
+ return 5;
170
+ }
171
+ // The host's own channel for this is the instructions the server sends on
172
+ // initialize, which reach the model. This line reaches whoever is reading
173
+ // the log, once per session rather than once per frame.
174
+ noteServerVersion(response.headers);
175
+ const notice = updateNotice();
176
+ if (notice !== undefined && !saidUpdate) {
177
+ saidUpdate = true;
178
+ options.error(`${notice}\n`);
179
+ }
180
+ if (response.status === 401) {
181
+ options.write(`${revokedConnectionFrame(frameId(line), options.controlPlane)}\n`);
182
+ options.error("Balladeer refused this agent connection. Rotate it in Balladeer.\n");
183
+ return 5;
184
+ }
185
+ // The version floor, answered in the host's own protocol. A stale forwarder
186
+ // that has been running against this control plane for months learns here
187
+ // what to run, and the host sees a sentence rather than a parse failure.
188
+ if (response.status === 426) {
189
+ const update = updateLine(await safeJson(response));
190
+ options.write(`${staleClientFrame(frameId(line), update)}\n`);
191
+ options.error(`This copy of the Balladeer command is too old for ${options.controlPlane}. Update it with: ${update}\n`);
192
+ return 3;
193
+ }
194
+ const text = await response.text();
195
+ if (Buffer.byteLength(text, "utf8") > MAX_RESPONSE_BYTES) {
196
+ options.error("Balladeer refused a response frame larger than 4 MiB.\n");
197
+ return 5;
198
+ }
199
+ options.write(`${text.replace(/\n+$/, "")}\n`);
200
+ }
201
+ return 0;
202
+ }
@@ -0,0 +1,74 @@
1
+ export type PrepareOptions = Readonly<{
2
+ controlPlane: string;
3
+ json: boolean;
4
+ promiseId: string;
5
+ /** Prepare a replacement, invalidating the packet nobody has spent. */
6
+ again: boolean;
7
+ /** The repository whose connection to use, named as `owner/name`. */
8
+ repo?: string;
9
+ /** The repository whose connection to use, named by id, as `mcp` takes it. */
10
+ repository?: string;
11
+ environment: NodeJS.ProcessEnv;
12
+ cwd: string;
13
+ write: (text: string) => void;
14
+ }>;
15
+ /**
16
+ * A refusal Balladeer wrote, as this terminal has to say it.
17
+ *
18
+ * Two changes and no others. The instants move onto the reader's own clock,
19
+ * because the server writes UTC and "already prepared by Robert Clark on
20
+ * 2026-09-07T18:05:10.698Z" reached somebody whose clock said two in the
21
+ * afternoon. And the argument is named the way this command takes it. Every
22
+ * other word, including what to do next and why, is the server's.
23
+ */
24
+ export declare function atThisTerminal(text: string): string;
25
+ /**
26
+ * The six keys the sealed run reads, and nothing else.
27
+ *
28
+ * The runner rejects an unknown field outright, so a metadata file that carried
29
+ * a helpful comment, a timestamp, or the whole packet would fail the customer's
30
+ * protected run for a reason that had nothing to do with their behavior. This
31
+ * command therefore writes the packet's own `qualificationMetadata` object and
32
+ * refuses to write anything it does not recognise.
33
+ */
34
+ declare const METADATA_KEYS: readonly ["schemaVersion", "workspaceLocator", "receiptId", "revisionId", "bindingId", "workflowDigest"];
35
+ type Metadata = Record<(typeof METADATA_KEYS)[number], string>;
36
+ export declare function readQualificationMetadata(structured: unknown): Readonly<{
37
+ ok: true;
38
+ value: Metadata;
39
+ }> | Readonly<{
40
+ ok: false;
41
+ reason: string;
42
+ }>;
43
+ /**
44
+ * Where the file goes, refusing any path that would leave this repository.
45
+ *
46
+ * The path comes off the server's answer, and the whole point of this command is
47
+ * that it writes a file the person did not type. A server that answered with
48
+ * `../../etc/something` would otherwise have this command write there, so the
49
+ * resolved path has to stay under the directory the command was run in.
50
+ */
51
+ export declare function metadataDestination(cwd: string, metadataPath: string): Readonly<{
52
+ ok: true;
53
+ path: string;
54
+ }> | Readonly<{
55
+ ok: false;
56
+ reason: string;
57
+ }>;
58
+ /**
59
+ * Prepares the one-time qualification setup for one promise, and writes the file
60
+ * where the sealed run reads it.
61
+ *
62
+ * It runs over this repository's own agent connection, which is what makes it
63
+ * usable at all: preparing a qualification setup is the agent's job, and the
64
+ * setup session that paired the machine expires. There is no sign-off here and
65
+ * no code to paste. Agreeing the meaning was the person's act; building the
66
+ * check that proves it is this command's caller's.
67
+ *
68
+ * The file is written once. A second run while nobody has published against the
69
+ * first packet is refused, naming who prepared it and when, and `--again` mints
70
+ * a replacement that invalidates the earlier one on the server as well as
71
+ * overwriting the file here.
72
+ */
73
+ export declare function runPrepare(options: PrepareOptions): Promise<number>;
74
+ export {};