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.
Files changed (47) hide show
  1. package/README.md +53 -32
  2. package/dist/agent.d.ts +5 -0
  3. package/dist/agent.js +13 -1
  4. package/dist/cli.d.ts +16 -0
  5. package/dist/cli.js +181 -24
  6. package/dist/client.d.ts +24 -2
  7. package/dist/client.js +34 -3
  8. package/dist/commands/affected.js +8 -7
  9. package/dist/commands/check-seals.js +4 -4
  10. package/dist/commands/discover.js +16 -7
  11. package/dist/commands/explain.d.ts +1 -1
  12. package/dist/commands/explain.js +1 -1
  13. package/dist/commands/invite.js +2 -1
  14. package/dist/commands/prepare.d.ts +74 -0
  15. package/dist/commands/prepare.js +218 -0
  16. package/dist/commands/propose.d.ts +10 -0
  17. package/dist/commands/propose.js +29 -6
  18. package/dist/commands/repositories.js +1 -0
  19. package/dist/commands/session.d.ts +35 -0
  20. package/dist/commands/session.js +131 -0
  21. package/dist/commands/setup.d.ts +29 -0
  22. package/dist/commands/setup.js +302 -92
  23. package/dist/commands/status.d.ts +16 -0
  24. package/dist/commands/status.js +106 -23
  25. package/dist/commands/touch-map.js +2 -2
  26. package/dist/commands/whoami.js +2 -1
  27. package/dist/conventions.d.ts +9 -1
  28. package/dist/conventions.js +9 -1
  29. package/dist/copy.d.ts +83 -7
  30. package/dist/copy.js +226 -29
  31. package/dist/desktop-config.d.ts +85 -0
  32. package/dist/desktop-config.js +217 -0
  33. package/dist/git.d.ts +15 -0
  34. package/dist/git.js +23 -0
  35. package/dist/legacy.d.ts +41 -0
  36. package/dist/legacy.js +143 -0
  37. package/dist/local-time.d.ts +66 -0
  38. package/dist/local-time.js +84 -0
  39. package/dist/mcp-config.d.ts +10 -0
  40. package/dist/mcp-config.js +8 -4
  41. package/dist/session.d.ts +84 -0
  42. package/dist/session.js +135 -0
  43. package/dist/store.d.ts +11 -1
  44. package/dist/store.js +18 -6
  45. package/dist/wire.d.ts +95 -4
  46. package/dist/wire.js +2 -1
  47. package/package.json +1 -1
@@ -0,0 +1,35 @@
1
+ export type SessionOptions = Readonly<{
2
+ controlPlane: string;
3
+ json: boolean;
4
+ /**
5
+ * Read the session out of the commit at HEAD and tell Balladeer which commit
6
+ * it wrote, instead of printing an id.
7
+ */
8
+ record: boolean;
9
+ /** Start a new session even though one is still current. */
10
+ fresh: boolean;
11
+ /** The repository whose connection to use, named as `owner/name`. */
12
+ repo?: string;
13
+ /** The repository whose connection to use, named by id, as `mcp` takes it. */
14
+ repository?: string;
15
+ environment: NodeJS.ProcessEnv;
16
+ cwd: string;
17
+ now?: string;
18
+ write: (text: string) => void;
19
+ }>;
20
+ /**
21
+ * The id that joins what an agent was told to what it then wrote.
22
+ *
23
+ * Retrieval happens before the commit exists, so nothing ties the two together
24
+ * on its own, and without the tie a red check on Tuesday cannot be read back
25
+ * against what Balladeer had said on Monday. The agent's own working session
26
+ * spans both: it reads the index under this id and writes the same id into the
27
+ * commit it produces.
28
+ *
29
+ * `balladeer session` prints the id and the exact line to write.
30
+ * `balladeer session --record` reads that line back out of the commit at HEAD
31
+ * and tells Balladeer which commit the session wrote. The commit message itself
32
+ * never leaves this machine: only the pair of a session id and a forty-
33
+ * character SHA is sent, and the SHA is a fact CI already reports.
34
+ */
35
+ export declare function runSession(options: SessionOptions): Promise<number>;
@@ -0,0 +1,131 @@
1
+ import { callAgentTool, noAgentCredentialSentence, reportAgentEnforcementWarning, selectAgent, } from "../agent.js";
2
+ import { headCommit } from "../git.js";
3
+ import { repositoryHint } from "../repository.js";
4
+ import { AGENT_SESSION_TRAILER, agentSessionTrailerLine, currentSession, readAgentSessionTrailer, } from "../session.js";
5
+ import { StoreError, readCredentials } from "../store.js";
6
+ import { CLI_INVOCATION } from "../wire.js";
7
+ /**
8
+ * The id that joins what an agent was told to what it then wrote.
9
+ *
10
+ * Retrieval happens before the commit exists, so nothing ties the two together
11
+ * on its own, and without the tie a red check on Tuesday cannot be read back
12
+ * against what Balladeer had said on Monday. The agent's own working session
13
+ * spans both: it reads the index under this id and writes the same id into the
14
+ * commit it produces.
15
+ *
16
+ * `balladeer session` prints the id and the exact line to write.
17
+ * `balladeer session --record` reads that line back out of the commit at HEAD
18
+ * and tells Balladeer which commit the session wrote. The commit message itself
19
+ * never leaves this machine: only the pair of a session id and a forty-
20
+ * character SHA is sent, and the SHA is a fact CI already reports.
21
+ */
22
+ export async function runSession(options) {
23
+ const emit = (step) => {
24
+ if (options.json)
25
+ options.write(`${JSON.stringify(step)}\n`);
26
+ };
27
+ const say = (text) => {
28
+ if (!options.json)
29
+ options.write(`${text}\n`);
30
+ };
31
+ const fail = (reason, message, exitCode) => {
32
+ if (options.json)
33
+ emit({ step: "error", reason, message, changed: false, exitCode });
34
+ else
35
+ options.write(`${message}\n`);
36
+ return exitCode;
37
+ };
38
+ let credentials;
39
+ try {
40
+ credentials = readCredentials(options.environment);
41
+ }
42
+ catch (error) {
43
+ return fail(error instanceof StoreError ? error.code : "credential_store_unusable", error instanceof StoreError ? error.message : String(error), 4);
44
+ }
45
+ const here = options.repo ?? repositoryHint(options.cwd);
46
+ const selection = selectAgent(credentials.agents, options.controlPlane, options.repository, here);
47
+ if (selection.kind === "refused") {
48
+ const message = selection.missingFor === undefined
49
+ ? selection.reason
50
+ : noAgentCredentialSentence(selection.missingFor);
51
+ return fail("no_agent_credential_on_this_machine", message, 4);
52
+ }
53
+ const agent = selection.agent;
54
+ if (options.record)
55
+ return recordCommit(options, agent, emit, say, fail);
56
+ let session;
57
+ try {
58
+ session = currentSession({
59
+ repository: agent.repositoryId,
60
+ now: options.now ?? new Date().toISOString(),
61
+ ...(options.fresh ? { forceNew: true } : {}),
62
+ environment: options.environment,
63
+ });
64
+ }
65
+ catch (error) {
66
+ // This catch follows successful credential selection and covers only the
67
+ // local marker write. It must never turn an auth or --record refusal into
68
+ // permission to continue, or print the freshly generated but unsaved ID.
69
+ const filesystemError = error instanceof StoreError && error.code === "credential_store_unwritable"
70
+ ? error.cause
71
+ : error;
72
+ const permissionCode = filesystemError instanceof Error && "code" in filesystemError
73
+ ? filesystemError.code
74
+ : undefined;
75
+ if (permissionCode === "EPERM" || permissionCode === "EACCES") {
76
+ return fail("session_store_permission_denied", `No session stamp was saved: local file permissions refused the write (${permissionCode}). Your saved authenticated connection is unchanged; this command has not checked it with the server. Continue already-authorized MCP reads, coding and explicitly requested capture without the optional session field. Do not invent an ID, add a session trailer, or run session --record for this unsaved stamp. Do not broaden filesystem access or move credentials to retry this write. Capture still requires the person's request or accepted offer, and human meaning approval is unchanged.`, 4);
77
+ }
78
+ return fail(error instanceof StoreError ? error.code : "session_store_unwritable", error instanceof StoreError ? error.message : String(error), 4);
79
+ }
80
+ const trailer = agentSessionTrailerLine(session.sessionId);
81
+ emit({
82
+ step: "session",
83
+ sessionId: session.sessionId,
84
+ startedAt: session.startedAt,
85
+ minted: session.minted,
86
+ trailer,
87
+ changed: session.minted,
88
+ });
89
+ say(session.sessionId);
90
+ say("");
91
+ say(session.minted
92
+ ? "This is a new session. Pass it to every promise read you make in this repository."
93
+ : "This session is already open. Pass it to every promise read you make in this repository, and use --new to start a different one.");
94
+ say(`Then write this line into the commit or pull-request body you produce:`);
95
+ say("");
96
+ say(` ${trailer}`);
97
+ say("");
98
+ say(`and run \`${CLI_INVOCATION} session --record\` once the commit exists, so a check that goes red on it can be read back against what you were told before you started. Balladeer is sent the session id and the commit SHA, and nothing else.`);
99
+ return 0;
100
+ }
101
+ async function recordCommit(options, agent, emit, say, fail) {
102
+ const head = await headCommit(options.cwd);
103
+ if (head === undefined) {
104
+ return fail("no_commit_here", "There is no commit to record: this directory is not a git checkout, or it has no commits yet.", 4);
105
+ }
106
+ const sessionId = readAgentSessionTrailer(head.message);
107
+ if (sessionId === undefined) {
108
+ // Said in full rather than as a code. The likeliest reader of this line is
109
+ // an agent that forgot the trailer, and the remedy is one line it can add
110
+ // to the commit it just made.
111
+ return fail("no_session_trailer", `The commit at HEAD carries no ${AGENT_SESSION_TRAILER} line this release recognises, so there is nothing to record. Add \`${AGENT_SESSION_TRAILER}: <the id ${CLI_INVOCATION} session prints>\` to the commit message and run this again. A commit carrying two different session ids is refused rather than guessed at.`, 4);
112
+ }
113
+ const call = await callAgentTool(agent, "record_session_commit", {
114
+ session: sessionId,
115
+ commitSha: head.sha,
116
+ });
117
+ reportAgentEnforcementWarning(call, options);
118
+ if (call.kind !== "result") {
119
+ return fail(call.kind === "tool_refusal" ? "refused" : call.kind, call.kind === "tool_refusal"
120
+ ? call.text
121
+ : `Balladeer could not record this commit (${call.kind}). Nothing was recorded, and running this again after the connection is back is safe: the same pair recorded twice is recorded once.`, 5);
122
+ }
123
+ emit({
124
+ step: "session_commit",
125
+ sessionId,
126
+ commitSha: head.sha,
127
+ changed: true,
128
+ });
129
+ say(`Recorded ${head.sha.slice(0, 12)} as written by this session.`);
130
+ return 0;
131
+ }
@@ -14,6 +14,12 @@ export type SetupOptions = Readonly<{
14
14
  * one checkout can honestly do from here, and the run says so per repository.
15
15
  */
16
16
  repositories?: readonly string[];
17
+ /**
18
+ * Connect an invited teammate to a repository the workspace already holds.
19
+ * This is deliberately a refusal boundary: if the repository is absent, the
20
+ * run stops rather than turning an invitation into authority to enroll it.
21
+ */
22
+ existingOnly?: boolean;
17
23
  /**
18
24
  * Repair the two files a previous setup wrote in this repository, and do
19
25
  * nothing else. No pairing, no enrollment, no credential, no network: a
@@ -21,6 +27,29 @@ export type SetupOptions = Readonly<{
21
27
  * reach the control plane at that moment.
22
28
  */
23
29
  refresh?: boolean;
30
+ /**
31
+ * Set up even though an earlier Balladeer is still installed on this machine.
32
+ *
33
+ * The check that stops this run is a warning about two programs claiming one
34
+ * name, not a safety property, so somebody who has decided to run both says so
35
+ * and this run carries on exactly as it would have.
36
+ */
37
+ force?: boolean;
38
+ /**
39
+ * Whether to also connect Claude desktop chat for this repository.
40
+ *
41
+ * Three states, because there are three answers and only two of them are a
42
+ * flag. `true` is `--claude-desktop`: connect it, and say out loud when this
43
+ * machine has no such app rather than skipping in silence. `false` is
44
+ * `--no-claude-desktop`: do not touch that file and do not mention it.
45
+ * Undefined is the ordinary run, which connects it where Claude desktop is
46
+ * installed and says nothing at all where it is not. Writing it by default is
47
+ * the same bargain `.mcp.json` already makes: the entry is additive, it
48
+ * carries no credential, it never replaces anybody else's server, and the
49
+ * people this is for write their plan in that chat before they open a coding
50
+ * agent at all.
51
+ */
52
+ claudeDesktop?: boolean;
24
53
  environment: NodeJS.ProcessEnv;
25
54
  cwd: string;
26
55
  write: (text: string) => void;