balladeer 1.0.5 → 1.0.7
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 +21 -4
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +39 -1
- package/dist/commands/guidance.d.ts +17 -0
- package/dist/commands/guidance.js +109 -0
- package/dist/commands/mcp.js +9 -0
- package/dist/commands/propose.js +7 -2
- package/dist/commands/setup.js +26 -3
- package/dist/conventions.d.ts +1 -1
- package/dist/conventions.js +1 -1
- package/dist/copy.d.ts +7 -5
- package/dist/copy.js +78 -85
- package/dist/guidance-hook.mjs +2972 -0
- package/dist/guidance-install.d.ts +29 -0
- package/dist/guidance-install.js +420 -0
- package/dist/guidance-runtime-entry.d.ts +1 -0
- package/dist/guidance-runtime-entry.js +2 -0
- package/dist/guidance-runtime.d.ts +15 -0
- package/dist/guidance-runtime.js +149 -0
- package/dist/guidance.d.ts +39 -0
- package/dist/guidance.js +293 -0
- package/dist/install.js +102 -12
- package/dist/wire.d.ts +8 -2
- package/dist/wire.js +1 -1
- package/package.json +4 -3
package/dist/guidance.js
ADDED
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
import { CLIENT_HEADER, CLIENT_HEADER_VALUE } from "./wire.js";
|
|
2
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
3
|
+
import { constants, closeSync, fstatSync, lstatSync, mkdirSync, openSync, readFileSync, renameSync, unlinkSync, writeFileSync, } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { configHome } from "./store.js";
|
|
6
|
+
export const GUIDANCE_UNAVAILABLE = "Balladeer could not verify the current workspace guidance and capture mode. Continue the user's authorized work, but do not make unsolicited capture offers or file inferred promises. Do not reuse earlier Quiet/Thorough permission or cached instructions as current. An explicit request to record still requires current Balladeer tool checks and named-human agreement to meaning; never invent approval. Retry current guidance at the next hook boundary.";
|
|
7
|
+
const HEX = /^[a-f0-9]{64}$/;
|
|
8
|
+
const UUID = /^[a-f0-9]{8}-[a-f0-9]{4}-[1-5][a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$/i;
|
|
9
|
+
const MAX_BYTES = 40_000;
|
|
10
|
+
const sha = (value) => createHash("sha256").update(value).digest("hex");
|
|
11
|
+
function object(value) {
|
|
12
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
13
|
+
}
|
|
14
|
+
export function parseGuidance(value, agent) {
|
|
15
|
+
if (!object(value) ||
|
|
16
|
+
Object.keys(value).sort().join(",") !==
|
|
17
|
+
"captureStyle,digest,instructions,repositoryId,revision,schemaVersion,workspaceId" ||
|
|
18
|
+
value.schemaVersion !== 1 ||
|
|
19
|
+
value.workspaceId !== agent.workspaceId ||
|
|
20
|
+
value.repositoryId !== agent.repositoryId ||
|
|
21
|
+
typeof value.revision !== "string" ||
|
|
22
|
+
!/^[A-Za-z0-9][A-Za-z0-9._/-]{0,127}$/.test(value.revision) ||
|
|
23
|
+
typeof value.digest !== "string" ||
|
|
24
|
+
!HEX.test(value.digest) ||
|
|
25
|
+
!["quiet", "thorough", "off"].includes(String(value.captureStyle)) ||
|
|
26
|
+
typeof value.instructions !== "string" ||
|
|
27
|
+
value.instructions.length === 0 ||
|
|
28
|
+
value.instructions.length > 8_000 ||
|
|
29
|
+
sha(value.instructions) !== value.digest) {
|
|
30
|
+
throw new Error("invalid_guidance");
|
|
31
|
+
}
|
|
32
|
+
return value;
|
|
33
|
+
}
|
|
34
|
+
export function guidanceEndpoint(agent) {
|
|
35
|
+
const url = new URL(agent.mcpUrl);
|
|
36
|
+
const expected = new URL(agent.controlPlane);
|
|
37
|
+
if (url.origin !== expected.origin ||
|
|
38
|
+
url.protocol !== "https:" ||
|
|
39
|
+
url.pathname !== "/api/mcp" ||
|
|
40
|
+
url.username ||
|
|
41
|
+
url.password ||
|
|
42
|
+
url.hash ||
|
|
43
|
+
!UUID.test(agent.workspaceId) ||
|
|
44
|
+
!UUID.test(agent.repositoryId))
|
|
45
|
+
throw new Error("invalid_guidance_scope");
|
|
46
|
+
for (const [key, value] of [
|
|
47
|
+
["workspaceId", agent.workspaceId],
|
|
48
|
+
["repositoryId", agent.repositoryId],
|
|
49
|
+
]) {
|
|
50
|
+
if (url.searchParams.getAll(key).length > 1 ||
|
|
51
|
+
(url.searchParams.has(key) && url.searchParams.get(key) !== value))
|
|
52
|
+
throw new Error("invalid_guidance_scope");
|
|
53
|
+
url.searchParams.set(key, value);
|
|
54
|
+
}
|
|
55
|
+
url.pathname = "/api/agent-guidance";
|
|
56
|
+
return url;
|
|
57
|
+
}
|
|
58
|
+
function readJson(path) {
|
|
59
|
+
let fd;
|
|
60
|
+
try {
|
|
61
|
+
fd = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
62
|
+
const stat = fstatSync(fd);
|
|
63
|
+
if (!stat.isFile() || stat.size > MAX_BYTES || (stat.mode & 0o077) !== 0)
|
|
64
|
+
return undefined;
|
|
65
|
+
return JSON.parse(readFileSync(fd, "utf8"));
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
70
|
+
finally {
|
|
71
|
+
if (fd !== undefined)
|
|
72
|
+
closeSync(fd);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
function privateDirectory(path) {
|
|
76
|
+
try {
|
|
77
|
+
mkdirSync(path, { mode: 0o700 });
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
/* Existing private directory is checked below. */
|
|
81
|
+
}
|
|
82
|
+
const stat = lstatSync(path);
|
|
83
|
+
if (!stat.isDirectory() || stat.isSymbolicLink() || (stat.mode & 0o077) !== 0)
|
|
84
|
+
throw new Error("cache_unavailable");
|
|
85
|
+
}
|
|
86
|
+
function atomicJson(path, value) {
|
|
87
|
+
const temporary = `${path}.${randomBytes(8).toString("hex")}.tmp`;
|
|
88
|
+
try {
|
|
89
|
+
writeFileSync(temporary, `${JSON.stringify(value)}\n`, { flag: "wx", mode: 0o600 });
|
|
90
|
+
renameSync(temporary, path);
|
|
91
|
+
}
|
|
92
|
+
finally {
|
|
93
|
+
try {
|
|
94
|
+
unlinkSync(temporary);
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
/* Renamed or never created. */
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
export function guidanceScopeKey(agent) {
|
|
102
|
+
return sha(JSON.stringify([
|
|
103
|
+
new URL(agent.controlPlane).origin,
|
|
104
|
+
sha(agent.token),
|
|
105
|
+
agent.workspaceId,
|
|
106
|
+
agent.repositoryId,
|
|
107
|
+
1,
|
|
108
|
+
]));
|
|
109
|
+
}
|
|
110
|
+
function cacheDirectory(environment) {
|
|
111
|
+
const home = configHome(environment);
|
|
112
|
+
privateDirectory(home);
|
|
113
|
+
const directory = join(home, "guidance-v1");
|
|
114
|
+
privateDirectory(directory);
|
|
115
|
+
return directory;
|
|
116
|
+
}
|
|
117
|
+
function cached(directory, scopeKey, agent) {
|
|
118
|
+
try {
|
|
119
|
+
const pointer = readJson(join(directory, `${scopeKey}.current.json`));
|
|
120
|
+
if (!object(pointer) || typeof pointer.key !== "string" || !HEX.test(pointer.key))
|
|
121
|
+
return undefined;
|
|
122
|
+
const body = readJson(join(directory, `${pointer.key}.body.json`));
|
|
123
|
+
if (!object(body) || typeof body.etag !== "string" || !/^"[a-f0-9]{64}"$/.test(body.etag))
|
|
124
|
+
return undefined;
|
|
125
|
+
const document = parseGuidance(body.document, agent);
|
|
126
|
+
if (pointer.key !== sha(JSON.stringify([scopeKey, document.revision])))
|
|
127
|
+
return undefined;
|
|
128
|
+
return { document, etag: body.etag, key: pointer.key };
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
return undefined;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
async function boundedBody(response) {
|
|
135
|
+
const declared = Number(response.headers.get("content-length") ?? 0);
|
|
136
|
+
if (declared > MAX_BYTES)
|
|
137
|
+
throw new Error("guidance_oversized");
|
|
138
|
+
const reader = response.body?.getReader();
|
|
139
|
+
if (!reader)
|
|
140
|
+
throw new Error("guidance_empty");
|
|
141
|
+
const buffers = [];
|
|
142
|
+
let bytes = 0;
|
|
143
|
+
try {
|
|
144
|
+
while (true) {
|
|
145
|
+
const part = await reader.read();
|
|
146
|
+
if (part.done)
|
|
147
|
+
break;
|
|
148
|
+
bytes += part.value.byteLength;
|
|
149
|
+
if (bytes > MAX_BYTES)
|
|
150
|
+
throw new Error("guidance_oversized");
|
|
151
|
+
buffers.push(part.value);
|
|
152
|
+
}
|
|
153
|
+
return JSON.parse(Buffer.concat(buffers).toString("utf8"));
|
|
154
|
+
}
|
|
155
|
+
finally {
|
|
156
|
+
await reader.cancel().catch(() => undefined);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
/** Never returns stale instructions: even a cache hit requires a fresh scoped server response. */
|
|
160
|
+
export async function loadGuidance(input) {
|
|
161
|
+
const scopeKey = guidanceScopeKey(input.agent);
|
|
162
|
+
let directory;
|
|
163
|
+
try {
|
|
164
|
+
directory = cacheDirectory(input.environment);
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
/* Cache is optional. */
|
|
168
|
+
}
|
|
169
|
+
const previous = directory ? cached(directory, scopeKey, input.agent) : undefined;
|
|
170
|
+
const abort = new AbortController();
|
|
171
|
+
let timer;
|
|
172
|
+
try {
|
|
173
|
+
const task = async () => {
|
|
174
|
+
const response = await (input.fetchImpl ?? fetch)(guidanceEndpoint(input.agent), {
|
|
175
|
+
method: "GET",
|
|
176
|
+
redirect: "manual",
|
|
177
|
+
signal: abort.signal,
|
|
178
|
+
headers: {
|
|
179
|
+
authorization: `Bearer ${input.agent.token}`,
|
|
180
|
+
[CLIENT_HEADER]: CLIENT_HEADER_VALUE,
|
|
181
|
+
accept: "application/json",
|
|
182
|
+
...(previous ? { "if-none-match": previous.etag } : {}),
|
|
183
|
+
},
|
|
184
|
+
});
|
|
185
|
+
if (response.status === 304) {
|
|
186
|
+
if (!previous ||
|
|
187
|
+
response.headers.get("etag") !== previous.etag ||
|
|
188
|
+
response.headers.get("x-balladeer-guidance-revision") !== previous.document.revision) {
|
|
189
|
+
throw new Error("invalid_guidance_revalidation");
|
|
190
|
+
}
|
|
191
|
+
return { status: "revalidated", document: previous.document, scopeKey };
|
|
192
|
+
}
|
|
193
|
+
if (response.status !== 200) {
|
|
194
|
+
await response.body?.cancel();
|
|
195
|
+
throw new Error("guidance_unavailable");
|
|
196
|
+
}
|
|
197
|
+
const document = parseGuidance(await boundedBody(response), input.agent);
|
|
198
|
+
const etag = response.headers.get("etag");
|
|
199
|
+
if (!etag ||
|
|
200
|
+
!/^"[a-f0-9]{64}"$/.test(etag) ||
|
|
201
|
+
response.headers.get("x-balladeer-guidance-revision") !== document.revision)
|
|
202
|
+
throw new Error("invalid_guidance_headers");
|
|
203
|
+
// Do not let a late, nonconforming transport populate cache after the deadline.
|
|
204
|
+
if (abort.signal.aborted)
|
|
205
|
+
throw new Error("guidance_deadline");
|
|
206
|
+
if (directory) {
|
|
207
|
+
try {
|
|
208
|
+
const key = sha(JSON.stringify([scopeKey, document.revision]));
|
|
209
|
+
atomicJson(join(directory, `${key}.body.json`), { document, etag });
|
|
210
|
+
atomicJson(join(directory, `${scopeKey}.current.json`), { key });
|
|
211
|
+
if (previous && previous.key !== key) {
|
|
212
|
+
try {
|
|
213
|
+
unlinkSync(join(directory, `${previous.key}.body.json`));
|
|
214
|
+
}
|
|
215
|
+
catch {
|
|
216
|
+
/* Concurrent cache is optional. */
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
catch {
|
|
221
|
+
/* Fresh guidance remains usable without disk caching. */
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return { status: "fresh", document, scopeKey };
|
|
225
|
+
};
|
|
226
|
+
return await Promise.race([
|
|
227
|
+
task(),
|
|
228
|
+
new Promise((_, reject) => {
|
|
229
|
+
timer = setTimeout(() => {
|
|
230
|
+
abort.abort();
|
|
231
|
+
reject(new Error("guidance_deadline"));
|
|
232
|
+
}, Math.min(input.timeoutMs ?? 1000, 1000));
|
|
233
|
+
}),
|
|
234
|
+
]);
|
|
235
|
+
}
|
|
236
|
+
catch {
|
|
237
|
+
abort.abort();
|
|
238
|
+
return { status: "unavailable", scopeKey };
|
|
239
|
+
}
|
|
240
|
+
finally {
|
|
241
|
+
if (timer)
|
|
242
|
+
clearTimeout(timer);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
/** Writes context first, then records emission metadata; this never claims model receipt. */
|
|
246
|
+
export function recordGuidanceContext(input, write) {
|
|
247
|
+
const always = input.event !== "UserPromptSubmit" || input.load.status === "unavailable" || !input.sessionId;
|
|
248
|
+
const contextKey = sha(JSON.stringify([
|
|
249
|
+
input.scopeKey,
|
|
250
|
+
input.hook,
|
|
251
|
+
input.sessionId ?? randomBytes(16).toString("hex"),
|
|
252
|
+
input.agentId ?? "root",
|
|
253
|
+
]));
|
|
254
|
+
const document = input.load.document;
|
|
255
|
+
let path;
|
|
256
|
+
let emitted = true;
|
|
257
|
+
try {
|
|
258
|
+
path = join(cacheDirectory(input.environment), `${contextKey}.context.json`);
|
|
259
|
+
const previous = readJson(path);
|
|
260
|
+
emitted =
|
|
261
|
+
always ||
|
|
262
|
+
!object(previous) ||
|
|
263
|
+
previous.status === "unavailable" ||
|
|
264
|
+
previous.digest !== document?.digest ||
|
|
265
|
+
previous.revision !== document?.revision ||
|
|
266
|
+
(previous.runtimeGeneration ?? null) !== (input.runtimeGeneration ?? null);
|
|
267
|
+
}
|
|
268
|
+
catch {
|
|
269
|
+
/* Cache failure requires emission. */
|
|
270
|
+
}
|
|
271
|
+
if (!emitted)
|
|
272
|
+
return false;
|
|
273
|
+
write();
|
|
274
|
+
if (path) {
|
|
275
|
+
try {
|
|
276
|
+
atomicJson(path, {
|
|
277
|
+
schemaVersion: 1,
|
|
278
|
+
contextKey,
|
|
279
|
+
event: input.event,
|
|
280
|
+
status: input.load.status,
|
|
281
|
+
digest: document?.digest ?? null,
|
|
282
|
+
revision: document?.revision ?? null,
|
|
283
|
+
runtimeGeneration: input.runtimeGeneration ?? null,
|
|
284
|
+
emitted: true,
|
|
285
|
+
emittedAt: new Date(input.now ?? Date.now()).toISOString(),
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
catch {
|
|
289
|
+
/* An unwritten receipt causes another emission, never a skipped one. */
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
return true;
|
|
293
|
+
}
|
package/dist/install.js
CHANGED
|
@@ -23,9 +23,10 @@ function commandOnPath(name, env) {
|
|
|
23
23
|
}
|
|
24
24
|
return undefined;
|
|
25
25
|
}
|
|
26
|
-
function execute(command, args, env) {
|
|
26
|
+
function execute(command, args, env, cwd) {
|
|
27
27
|
return execFileSync(command, args, {
|
|
28
28
|
env,
|
|
29
|
+
...(cwd ? { cwd } : {}),
|
|
29
30
|
encoding: "utf8",
|
|
30
31
|
stdio: ["ignore", "pipe", "pipe"],
|
|
31
32
|
timeout: 120_000,
|
|
@@ -33,14 +34,14 @@ function execute(command, args, env) {
|
|
|
33
34
|
}).trim();
|
|
34
35
|
}
|
|
35
36
|
/** Inspect package ownership before executing a command from PATH. */
|
|
36
|
-
function packageVersion(command, env) {
|
|
37
|
+
function packageVersion(command, env, cwd) {
|
|
37
38
|
let entry = realpathSync(command);
|
|
38
39
|
if (basename(entry) === "volta-shim") {
|
|
39
40
|
const volta = commandOnPath("volta", env);
|
|
40
41
|
if (!volta)
|
|
41
42
|
return undefined;
|
|
42
43
|
try {
|
|
43
|
-
entry = realpathSync(execute(volta, ["which", "balladeer"], env));
|
|
44
|
+
entry = realpathSync(execute(volta, ["which", "balladeer"], env, cwd));
|
|
44
45
|
}
|
|
45
46
|
catch {
|
|
46
47
|
return undefined;
|
|
@@ -121,25 +122,114 @@ function shellProfiles(env, bin) {
|
|
|
121
122
|
return { path, text, previous: old };
|
|
122
123
|
});
|
|
123
124
|
}
|
|
125
|
+
function atLeastCurrent(version) {
|
|
126
|
+
if (!/^\d+\.\d+\.\d+$/.test(version))
|
|
127
|
+
return false;
|
|
128
|
+
const actual = version.split(".").map(Number), expected = CLI_VERSION.split(".").map(Number);
|
|
129
|
+
for (let i = 0; i < 3; i++) {
|
|
130
|
+
if (actual[i] !== expected[i])
|
|
131
|
+
return actual[i] > expected[i];
|
|
132
|
+
}
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
/** npm exec may put the actual Node-image bin ahead of Volta's registered shim. */
|
|
136
|
+
function installWithVolta(env) {
|
|
137
|
+
const home = env.VOLTA_HOME ?? (env.HOME ? join(env.HOME, ".volta") : undefined);
|
|
138
|
+
const volta = commandOnPath("volta", env);
|
|
139
|
+
if (!home || !isAbsolute(home) || !volta)
|
|
140
|
+
return undefined;
|
|
141
|
+
const bin = join(home, "bin"), shim = join(bin, "balladeer");
|
|
142
|
+
const nodeShim = join(bin, "node");
|
|
143
|
+
// Merely having Volta somewhere on PATH must not select it for another toolchain.
|
|
144
|
+
const onPath = (env.PATH ?? "")
|
|
145
|
+
.split(delimiter)
|
|
146
|
+
.some((part) => resolve(part || ".") === resolve(bin));
|
|
147
|
+
const managedNode = existsSync(nodeShim) && basename(realpathSync(nodeShim)) === "volta-shim";
|
|
148
|
+
const imageDirectory = join(home, "tools/image/node");
|
|
149
|
+
const imageRoot = existsSync(imageDirectory)
|
|
150
|
+
? realpathSync(imageDirectory)
|
|
151
|
+
: resolve(imageDirectory);
|
|
152
|
+
const selectedNode = commandOnPath("node", env);
|
|
153
|
+
const actualNode = selectedNode ? realpathSync(selectedNode) : undefined;
|
|
154
|
+
const activeToolchain = actualNode !== undefined &&
|
|
155
|
+
((managedNode && actualNode === realpathSync(nodeShim)) ||
|
|
156
|
+
actualNode.startsWith(imageRoot + "/"));
|
|
157
|
+
if (!onPath || !managedNode || !activeToolchain)
|
|
158
|
+
return undefined;
|
|
159
|
+
const bootstrap = env.npm_command === "exec";
|
|
160
|
+
const parentEnv = {
|
|
161
|
+
...env,
|
|
162
|
+
PATH: (env.PATH ?? "")
|
|
163
|
+
.split(delimiter)
|
|
164
|
+
.filter((part) => {
|
|
165
|
+
const full = resolve(part || ".");
|
|
166
|
+
if (full.split(/[\\/]/).includes("node_modules"))
|
|
167
|
+
return false;
|
|
168
|
+
return !(bootstrap &&
|
|
169
|
+
(existsSync(full) ? realpathSync(full) : full).startsWith(imageRoot + "/") &&
|
|
170
|
+
basename(full) === "bin");
|
|
171
|
+
})
|
|
172
|
+
.join(delimiter),
|
|
173
|
+
};
|
|
174
|
+
const selected = commandOnPath("balladeer", parentEnv);
|
|
175
|
+
if (selected && resolve(selected) !== resolve(shim))
|
|
176
|
+
throw new InstallError(`Another command precedes Volta at ${selected}. Resolve that PATH conflict before installing; nothing was overwritten.`);
|
|
177
|
+
if (existsSync(shim) && basename(realpathSync(shim)) !== "volta-shim")
|
|
178
|
+
throw new InstallError("Another command owns Volta's balladeer path. Nothing was overwritten.");
|
|
179
|
+
const neutral = mkdtempSync(join(tmpdir(), "balladeer-volta-install-"));
|
|
180
|
+
const installEnv = { ...parentEnv, npm_config_cache: join(neutral, "npm-cache") };
|
|
181
|
+
try {
|
|
182
|
+
let version = existsSync(shim) ? packageVersion(shim, installEnv, neutral) : undefined;
|
|
183
|
+
if (existsSync(shim) && version === undefined)
|
|
184
|
+
throw new InstallError("Volta's registered Balladeer package cannot be verified. Repair the toolchain before pairing.");
|
|
185
|
+
const alreadyCurrent = version !== undefined && atLeastCurrent(version);
|
|
186
|
+
if (!alreadyCurrent) {
|
|
187
|
+
try {
|
|
188
|
+
execute(volta, ["install", `balladeer@${CLI_VERSION}`], installEnv, neutral);
|
|
189
|
+
}
|
|
190
|
+
catch {
|
|
191
|
+
throw new InstallError("Volta could not install the exact Balladeer release. Check network and toolchain write access, then repeat npx -y balladeer@latest install. No pairing has started.");
|
|
192
|
+
}
|
|
193
|
+
version = existsSync(shim) ? packageVersion(shim, installEnv, neutral) : undefined;
|
|
194
|
+
if (version !== CLI_VERSION)
|
|
195
|
+
throw new InstallError("Volta finished, but its registered default is not the expected Balladeer release. No pairing has started.");
|
|
196
|
+
}
|
|
197
|
+
// Verify both the global default and what an ordinary fresh child in the
|
|
198
|
+
// caller's directory sees. A project override is never mistaken for a default.
|
|
199
|
+
if (!version ||
|
|
200
|
+
execute(shim, ["--version"], installEnv, neutral) !== version ||
|
|
201
|
+
commandOnPath("balladeer", parentEnv) !== shim ||
|
|
202
|
+
execute(shim, ["--version"], parentEnv) !== version)
|
|
203
|
+
throw new InstallError("Volta's default was checked, but this terminal or project selects a different Balladeer. Resolve its PATH or project tool pin before pairing.");
|
|
204
|
+
const shell = execute("/bin/sh", ["-c", "command -v balladeer; balladeer --version"], parentEnv).split("\n");
|
|
205
|
+
if (shell[0] !== shim || shell[1] !== version)
|
|
206
|
+
throw new InstallError("The fresh terminal PATH does not select the verified Volta command. No pairing has started.");
|
|
207
|
+
env.PATH = parentEnv.PATH;
|
|
208
|
+
return {
|
|
209
|
+
status: alreadyCurrent ? "current" : "installed",
|
|
210
|
+
command: shim,
|
|
211
|
+
version,
|
|
212
|
+
restartRequired: false,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
finally {
|
|
216
|
+
rmSync(neutral, { recursive: true, force: true });
|
|
217
|
+
}
|
|
218
|
+
}
|
|
124
219
|
export function ensureInstalled(options) {
|
|
125
220
|
const env = options.environment;
|
|
126
221
|
const platform = options.platform ?? process.platform;
|
|
127
222
|
if (platform === "win32")
|
|
128
223
|
throw new InstallError("Automatic command installation currently supports macOS and Linux. On Windows, use npm install --global balladeer@latest, then verify balladeer --version. No files or pairing were changed.");
|
|
224
|
+
const managed = installWithVolta(env);
|
|
225
|
+
if (managed)
|
|
226
|
+
return managed;
|
|
129
227
|
const current = commandOnPath("balladeer", env);
|
|
130
228
|
if (current) {
|
|
131
229
|
const version = packageVersion(current, env);
|
|
132
230
|
if (version === undefined)
|
|
133
231
|
throw new InstallError(`Another command already owns ${current}. Keep it intact and resolve that command conflict before installing Balladeer.`);
|
|
134
|
-
|
|
135
|
-
version
|
|
136
|
-
.split(".")
|
|
137
|
-
.map(Number)
|
|
138
|
-
.some((part, index, parts) => part > Number(CLI_VERSION.split(".")[index]) &&
|
|
139
|
-
parts
|
|
140
|
-
.slice(0, index)
|
|
141
|
-
.every((value, before) => value === Number(CLI_VERSION.split(".")[before])));
|
|
142
|
-
if (version === CLI_VERSION || newer) {
|
|
232
|
+
if (atLeastCurrent(version)) {
|
|
143
233
|
if (execute(current, ["--version"], env) !== version)
|
|
144
234
|
throw new InstallError("The installed Balladeer command does not run the expected version. Repair the installation before pairing.");
|
|
145
235
|
return { status: "current", command: current, restartRequired: false, version };
|
package/dist/wire.d.ts
CHANGED
|
@@ -5,10 +5,10 @@
|
|
|
5
5
|
* runtime dependency at all: Node 22 builtins and global fetch, nothing else.
|
|
6
6
|
* A contract test compares the scope list below against the server's.
|
|
7
7
|
*/
|
|
8
|
-
export declare const CLI_VERSION = "1.0.
|
|
8
|
+
export declare const CLI_VERSION = "1.0.7";
|
|
9
9
|
export declare const CLI_INVOCATION = "npx -y balladeer@latest";
|
|
10
10
|
export declare const CLIENT_HEADER = "x-balladeer-client";
|
|
11
|
-
export declare const CLIENT_HEADER_VALUE = "balladeer/1.0.
|
|
11
|
+
export declare const CLIENT_HEADER_VALUE = "balladeer/1.0.7";
|
|
12
12
|
export declare const DEFAULT_CONTROL_PLANE = "https://envelopes.balladeer.ai";
|
|
13
13
|
export type DelegatedScope = "repository:enroll" | "agent:issue" | "ci:connect" | "workspace:invite" | "candidate:propose";
|
|
14
14
|
export declare const DELEGATED_SCOPES: readonly DelegatedScope[];
|
|
@@ -222,6 +222,12 @@ export type ClaudeDesktopStep = Readonly<{
|
|
|
222
222
|
reason?: string;
|
|
223
223
|
}>;
|
|
224
224
|
export type JsonStep = Readonly<{
|
|
225
|
+
step: "guidance_loader";
|
|
226
|
+
status: "installed_needs_host_trust" | "installed_trust_unverified" | "unavailable" | "not_applicable";
|
|
227
|
+
changed: boolean;
|
|
228
|
+
hosts: readonly ("codex" | "claude")[];
|
|
229
|
+
reason?: string;
|
|
230
|
+
}> | Readonly<{
|
|
225
231
|
step: "explain";
|
|
226
232
|
version: string;
|
|
227
233
|
}> | Readonly<{
|
package/dist/wire.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* runtime dependency at all: Node 22 builtins and global fetch, nothing else.
|
|
6
6
|
* A contract test compares the scope list below against the server's.
|
|
7
7
|
*/
|
|
8
|
-
export const CLI_VERSION = "1.0.
|
|
8
|
+
export const CLI_VERSION = "1.0.7";
|
|
9
9
|
export const CLI_INVOCATION = "npx -y balladeer@latest";
|
|
10
10
|
export const CLIENT_HEADER = "x-balladeer-client";
|
|
11
11
|
export const CLIENT_HEADER_VALUE = `balladeer/${CLI_VERSION}`;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "balladeer",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.7",
|
|
4
4
|
"description": "Set up Balladeer from your terminal, or from a coding agent's.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"private": false,
|
|
@@ -21,11 +21,12 @@
|
|
|
21
21
|
"node": ">=22.0.0"
|
|
22
22
|
},
|
|
23
23
|
"scripts": {
|
|
24
|
-
"build": "tsc -p tsconfig.json",
|
|
24
|
+
"build": "tsc -p tsconfig.json && node build-guidance.mjs",
|
|
25
25
|
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
|
-
"typescript": "5.9.3"
|
|
28
|
+
"typescript": "5.9.3",
|
|
29
|
+
"esbuild": "0.25.12"
|
|
29
30
|
},
|
|
30
31
|
"dependencies": {
|
|
31
32
|
"@iarna/toml": "2.2.5"
|