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
|
@@ -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
|
+
}
|
package/dist/commands/setup.d.ts
CHANGED
|
@@ -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;
|