balladeer 1.0.8 → 1.0.11
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/dist/agent.d.ts +1 -1
- package/dist/agent.js +12 -0
- package/dist/cli.d.ts +3 -2
- package/dist/cli.js +117 -56
- package/dist/commands/discover.js +2 -2
- package/dist/commands/guidance.d.ts +11 -1
- package/dist/commands/guidance.js +24 -11
- package/dist/commands/mcp.js +2 -2
- package/dist/commands/prepare.js +2 -2
- package/dist/commands/propose.js +2 -2
- package/dist/commands/session.js +2 -2
- package/dist/commands/setup.d.ts +15 -0
- package/dist/commands/setup.js +51 -15
- package/dist/commands/status.js +2 -2
- package/dist/currency.d.ts +3 -0
- package/dist/currency.js +8 -0
- package/dist/guidance-hook.mjs +99 -46
- package/dist/guidance.js +4 -0
- package/dist/install.d.ts +2 -0
- package/dist/install.js +2 -0
- package/dist/open-browser.d.ts +13 -0
- package/dist/open-browser.js +30 -0
- package/dist/release.d.ts +6 -0
- package/dist/release.js +6 -0
- package/dist/remove-earlier.d.ts +68 -0
- package/dist/remove-earlier.js +378 -0
- package/dist/repository.d.ts +9 -0
- package/dist/repository.js +31 -5
- package/dist/self-update.d.ts +11 -0
- package/dist/self-update.js +59 -0
- package/dist/user-scope.d.ts +23 -7
- package/dist/user-scope.js +50 -17
- package/dist/wire.d.ts +15 -2
- package/dist/wire.js +1 -1
- package/package.json +1 -1
package/dist/agent.d.ts
CHANGED
|
@@ -41,7 +41,7 @@ export type AgentSelection = Readonly<{
|
|
|
41
41
|
* points somewhere other than the control plane it belongs to is refused: a
|
|
42
42
|
* server response must not be able to send this credential to a new host.
|
|
43
43
|
*/
|
|
44
|
-
export declare function selectAgent(agents: readonly StoredAgent[], controlPlane: string, repositoryId: string | undefined, currentRepository: string): AgentSelection;
|
|
44
|
+
export declare function selectAgent(agents: readonly StoredAgent[], controlPlane: string, repositoryId: string | undefined, currentRepository: string | readonly string[]): AgentSelection;
|
|
45
45
|
/**
|
|
46
46
|
* What a command says when this machine holds no credential for the repository.
|
|
47
47
|
*
|
package/dist/agent.js
CHANGED
|
@@ -20,6 +20,18 @@ const REPOSITORY_NAME = /^[A-Za-z0-9._-]{1,39}\/[A-Za-z0-9._-]{1,100}$/;
|
|
|
20
20
|
*/
|
|
21
21
|
export function selectAgent(agents, controlPlane, repositoryId, currentRepository) {
|
|
22
22
|
const here = agents.filter((agent) => agent.controlPlane === controlPlane);
|
|
23
|
+
// Several remotes, as `repositoryHints` lists them: the first with a
|
|
24
|
+
// connection is the folder's repository. One string is the older one-remote
|
|
25
|
+
// form and every caller of it still works.
|
|
26
|
+
if (typeof currentRepository !== "string") {
|
|
27
|
+
const candidates = currentRepository.filter((name) => REPOSITORY_NAME.test(name) && name !== "unknown/unknown");
|
|
28
|
+
for (const name of candidates) {
|
|
29
|
+
const found = selectAgent(agents, controlPlane, repositoryId, name);
|
|
30
|
+
if (found.kind !== "refused")
|
|
31
|
+
return found;
|
|
32
|
+
}
|
|
33
|
+
return selectAgent(agents, controlPlane, repositoryId, candidates[0] ?? "unknown/unknown");
|
|
34
|
+
}
|
|
23
35
|
if (repositoryId !== undefined) {
|
|
24
36
|
const named = here.find((agent) => agent.repositoryId === repositoryId ||
|
|
25
37
|
agent.repository?.toLowerCase() === repositoryId.toLowerCase());
|
package/dist/cli.d.ts
CHANGED
|
@@ -4,11 +4,13 @@ type Parsed = Readonly<{
|
|
|
4
4
|
subcommand: string | undefined;
|
|
5
5
|
json: boolean;
|
|
6
6
|
wait: boolean;
|
|
7
|
+
/** `setup --no-open`: print the approval link instead of opening it. */
|
|
8
|
+
noOpen: boolean;
|
|
7
9
|
/** Repair the files a previous setup wrote, and do nothing else. */
|
|
8
10
|
refresh: boolean;
|
|
9
11
|
client: "codex" | "claude" | undefined;
|
|
10
12
|
chooseWorkspace: boolean;
|
|
11
|
-
/** `setup --force`: set up
|
|
13
|
+
/** `setup --force`: set up alongside an earlier Balladeer instead of removing it. */
|
|
12
14
|
force: boolean;
|
|
13
15
|
/** `setup --existing`: connect only when this checkout is already enrolled. */
|
|
14
16
|
existingOnly: boolean;
|
|
@@ -19,7 +21,6 @@ type Parsed = Readonly<{
|
|
|
19
21
|
*/
|
|
20
22
|
claudeDesktop: boolean | undefined;
|
|
21
23
|
repo: string | undefined;
|
|
22
|
-
repoScope: boolean;
|
|
23
24
|
file: string | undefined;
|
|
24
25
|
repository: string | undefined;
|
|
25
26
|
/**
|
package/dist/cli.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
2
3
|
import { realpathSync } from "node:fs";
|
|
3
4
|
import { resolve } from "node:path";
|
|
4
5
|
import { fileURLToPath } from "node:url";
|
|
@@ -18,9 +19,10 @@ import { runStatus } from "./commands/status.js";
|
|
|
18
19
|
import { runTouchMap } from "./commands/touch-map.js";
|
|
19
20
|
import { runWhoami } from "./commands/whoami.js";
|
|
20
21
|
import { runInstall } from "./install.js";
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
22
|
+
import { PUBLISHED_SPECIFIER } from "./release.js";
|
|
23
|
+
import { installUserScope, runningFromCheckout, userHome, } from "./user-scope.js";
|
|
23
24
|
import { updateNotice } from "./currency.js";
|
|
25
|
+
import { describeEarlier, describeRemoval, findEarlier, removeEarlier } from "./remove-earlier.js";
|
|
24
26
|
import { StoreError, normalizeControlPlane } from "./store.js";
|
|
25
27
|
import { CLI_INVOCATION, CLI_VERSION, DEFAULT_CONTROL_PLANE } from "./wire.js";
|
|
26
28
|
const USAGE = `balladeer ${CLI_VERSION}
|
|
@@ -31,13 +33,19 @@ const USAGE = `balladeer ${CLI_VERSION}
|
|
|
31
33
|
it decides per folder, from the git remote, which connection applies.
|
|
32
34
|
Pairs nothing and changes no workspace, repository, or CI configuration.
|
|
33
35
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
stops it for this folder (or --repo, for every checkout of this
|
|
38
|
-
repository); track undoes it.
|
|
36
|
+
balladeer update
|
|
37
|
+
Move the durable balladeer command to the latest published release. The
|
|
38
|
+
hooks and MCP entries already run the latest release on their own.
|
|
39
39
|
|
|
40
|
-
|
|
40
|
+
balladeer remove-earlier
|
|
41
|
+
Take the earlier Balladeer (the July client) off this machine and out of
|
|
42
|
+
this repository: its MCP server under the same name, its hooks and status
|
|
43
|
+
line, its guidance blocks and managed files (under your home and in this
|
|
44
|
+
repository's CLAUDE.md or AGENTS.md), and its 0.x command. A file git
|
|
45
|
+
does not track is copied beside itself first; a tracked one is for you
|
|
46
|
+
to commit. setup runs this itself when it finds one.
|
|
47
|
+
|
|
48
|
+
${CLI_INVOCATION} setup [--repository owner/name]... [--json] [--wait] [--no-open] [--control-plane <url>]
|
|
41
49
|
[--create-workspace <name>] [--choose-workspace] [--refresh] [--force]
|
|
42
50
|
[--client codex|claude]
|
|
43
51
|
[--existing]
|
|
@@ -68,9 +76,12 @@ const USAGE = `balladeer ${CLI_VERSION}
|
|
|
68
76
|
other entry in those files alone, and prints what it changed. Installing
|
|
69
77
|
the command may contact npm; refresh never contacts Balladeer. Run it when
|
|
70
78
|
Balladeer says a newer version is available.
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
79
|
+
At a terminal with a person at it, the approval page opens in their
|
|
80
|
+
browser and the run waits for the approval; --no-open prints the link
|
|
81
|
+
instead. An agent shell, or --json, always prints the link and returns.
|
|
82
|
+
--force sets up alongside an earlier Balladeer instead of removing it.
|
|
83
|
+
Without it, a run that finds the older client takes it off first, with a
|
|
84
|
+
copy of every file it changes kept beside the file.
|
|
74
85
|
Claude desktop chat is connected too, under a key named for this
|
|
75
86
|
repository, wherever the app is installed. --claude-desktop asks for it by
|
|
76
87
|
name, so a machine with no Claude desktop on it says so instead of
|
|
@@ -167,7 +178,7 @@ const USAGE = `balladeer ${CLI_VERSION}
|
|
|
167
178
|
Exit codes: 0 progress reported truthfully, 2 pairing expired or denied or already
|
|
168
179
|
claimed, 3 this copy is too old for the server, 4 usage or credential store problem,
|
|
169
180
|
5 transport or server failure, 6 an earlier Balladeer is still installed on this
|
|
170
|
-
machine and
|
|
181
|
+
machine and part of it could not be removed.
|
|
171
182
|
`;
|
|
172
183
|
const SUBCOMMANDS = { agent: ["rotate"] };
|
|
173
184
|
export function parseArguments(argv) {
|
|
@@ -184,6 +195,7 @@ export function parseArguments(argv) {
|
|
|
184
195
|
let event;
|
|
185
196
|
let json = false;
|
|
186
197
|
let wait = false;
|
|
198
|
+
let noOpen = false;
|
|
187
199
|
let refresh = false;
|
|
188
200
|
let client;
|
|
189
201
|
let chooseWorkspace = false;
|
|
@@ -197,7 +209,6 @@ export function parseArguments(argv) {
|
|
|
197
209
|
let runner;
|
|
198
210
|
let controlPlane;
|
|
199
211
|
let repo;
|
|
200
|
-
let repoScope = false;
|
|
201
212
|
let file;
|
|
202
213
|
let owner;
|
|
203
214
|
let createWorkspace;
|
|
@@ -257,6 +268,8 @@ export function parseArguments(argv) {
|
|
|
257
268
|
runner = value("--runner", inline);
|
|
258
269
|
else if (name === "--wait")
|
|
259
270
|
wait = true;
|
|
271
|
+
else if (name === "--no-open")
|
|
272
|
+
noOpen = true;
|
|
260
273
|
else if (name === "--refresh")
|
|
261
274
|
refresh = true;
|
|
262
275
|
else if (name === "--choose-workspace")
|
|
@@ -277,8 +290,6 @@ export function parseArguments(argv) {
|
|
|
277
290
|
claudeDesktop = false;
|
|
278
291
|
else if (name === "--control-plane")
|
|
279
292
|
controlPlane = value("--control-plane", inline);
|
|
280
|
-
else if (name === "--repo" && (command === "quiet" || command === "track"))
|
|
281
|
-
repoScope = true;
|
|
282
293
|
else if (name === "--repo")
|
|
283
294
|
repo = value("--repo", inline);
|
|
284
295
|
else if (name === "--file")
|
|
@@ -373,6 +384,7 @@ export function parseArguments(argv) {
|
|
|
373
384
|
subcommand,
|
|
374
385
|
json,
|
|
375
386
|
wait,
|
|
387
|
+
noOpen,
|
|
376
388
|
refresh,
|
|
377
389
|
client,
|
|
378
390
|
chooseWorkspace,
|
|
@@ -380,7 +392,6 @@ export function parseArguments(argv) {
|
|
|
380
392
|
existingOnly,
|
|
381
393
|
claudeDesktop,
|
|
382
394
|
repo,
|
|
383
|
-
repoScope,
|
|
384
395
|
file,
|
|
385
396
|
repository: repositories[0],
|
|
386
397
|
repositories,
|
|
@@ -418,64 +429,114 @@ export async function main(argv) {
|
|
|
418
429
|
process.stderr.write(`${notice}\n`);
|
|
419
430
|
return code;
|
|
420
431
|
}
|
|
432
|
+
/** What `balladeer --version` answers on this PATH, or nothing. */
|
|
433
|
+
function commandVersion(name) {
|
|
434
|
+
const result = spawnSync(name, ["--version"], { encoding: "utf8", env: process.env });
|
|
435
|
+
const text = result.status === 0 ? result.stdout.trim() : "";
|
|
436
|
+
return /^\d+\.\d+\.\d+/.test(text) ? text : undefined;
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* Once per laptop: the durable command, then the same server and hook
|
|
440
|
+
* registered where every session on this machine reads them, naming that
|
|
441
|
+
* command by path so a session start costs node's startup and no npx. When
|
|
442
|
+
* the durable install is refused, the entries are still written, on the
|
|
443
|
+
* `npx` form of the current major: a blocked PATH must not keep a laptop from
|
|
444
|
+
* getting them. Nothing here needs approving in the coding host; Claude Code
|
|
445
|
+
* runs hooks from a person's own settings as they are.
|
|
446
|
+
*/
|
|
447
|
+
function installEverything(options) {
|
|
448
|
+
const published = !runningFromCheckout();
|
|
449
|
+
let writes;
|
|
450
|
+
const register = (installed) => {
|
|
451
|
+
writes = installUserScope({
|
|
452
|
+
environment: process.env,
|
|
453
|
+
published,
|
|
454
|
+
...(installed === undefined ? {} : { installed }),
|
|
455
|
+
});
|
|
456
|
+
return writes;
|
|
457
|
+
};
|
|
458
|
+
const code = runInstall({
|
|
459
|
+
environment: process.env,
|
|
460
|
+
json: options.json,
|
|
461
|
+
write: options.write,
|
|
462
|
+
allowPartial: options.allowPartial,
|
|
463
|
+
onInstalled: (result) => ({ userScope: register(result.command) }),
|
|
464
|
+
});
|
|
465
|
+
if (writes === undefined) {
|
|
466
|
+
const fallback = register();
|
|
467
|
+
if (options.json)
|
|
468
|
+
options.write(`${JSON.stringify({ step: "user_scope", userScope: fallback })}\n`);
|
|
469
|
+
}
|
|
470
|
+
for (const w of writes ?? [])
|
|
471
|
+
if (!options.json)
|
|
472
|
+
options.write(`${w.status === "refused" ? "Not changed" : w.status === "written" ? "Registered" : "Already current"}: ${w.path}${w.reason ? ` (${w.reason})` : ""}\n`);
|
|
473
|
+
if (!options.json && (writes ?? []).some((w) => w.status === "written"))
|
|
474
|
+
options.write("Balladeer now runs in every Claude Code" +
|
|
475
|
+
((writes ?? []).some((w) => w.host === "codex") ? " and Codex" : "") +
|
|
476
|
+
" session on this machine. In a folder of a connected repository it works as before; anywhere else it stays quiet, and `balladeer status` there says why.\n");
|
|
477
|
+
return (writes ?? []).some((w) => w.status === "refused") ? 4 : code;
|
|
478
|
+
}
|
|
421
479
|
async function dispatch(parsed, write) {
|
|
422
480
|
switch (parsed.command) {
|
|
423
481
|
case "explain":
|
|
424
482
|
return runExplain(write, parsed.controlPlane);
|
|
425
|
-
case "
|
|
426
|
-
//
|
|
427
|
-
//
|
|
428
|
-
//
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
published: !runningFromCheckout(),
|
|
483
|
+
case "update": {
|
|
484
|
+
// The latest published copy installs itself as the durable command, so
|
|
485
|
+
// this is one child process and its own report. Old and new are read
|
|
486
|
+
// from the command on PATH, before and after.
|
|
487
|
+
const before = commandVersion("balladeer");
|
|
488
|
+
const result = spawnSync("npx", ["-y", PUBLISHED_SPECIFIER, "install"], {
|
|
489
|
+
stdio: "inherit",
|
|
490
|
+
env: process.env,
|
|
434
491
|
});
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
492
|
+
if (result.status !== 0) {
|
|
493
|
+
write(`The update did not finish (exit ${result.status ?? "signal"}). Nothing else was changed.\n`);
|
|
494
|
+
return result.status ?? 5;
|
|
495
|
+
}
|
|
496
|
+
const after = commandVersion("balladeer");
|
|
497
|
+
write(before === after
|
|
498
|
+
? `balladeer is already ${after ?? "installed"}.\n`
|
|
499
|
+
: `balladeer ${before ?? "(not on PATH)"} is now ${after ?? "installed"}.\n`);
|
|
500
|
+
return 0;
|
|
501
|
+
}
|
|
502
|
+
case "remove-earlier": {
|
|
503
|
+
const earlier = findEarlier({
|
|
504
|
+
home: userHome(process.env),
|
|
505
|
+
controlPlane: parsed.controlPlane,
|
|
443
506
|
environment: process.env,
|
|
444
|
-
|
|
445
|
-
write,
|
|
446
|
-
extra: { userScope: writes },
|
|
507
|
+
cwd: process.cwd(),
|
|
447
508
|
});
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
case "quiet":
|
|
451
|
-
case "track": {
|
|
452
|
-
const scope = parsed.repoScope ? "repo" : "folder";
|
|
453
|
-
try {
|
|
454
|
-
const { key, changed } = setQuiet(process.cwd(), scope, parsed.command === "quiet", process.env);
|
|
455
|
-
write(parsed.command === "quiet"
|
|
456
|
-
? `${changed ? "Balladeer will stay quiet" : "Balladeer was already quiet"} in ${scope === "repo" ? `every checkout of ${key}` : key}. Run \`balladeer track${scope === "repo" ? " --repo" : ""}\` here to undo.\n`
|
|
457
|
-
: `${changed ? "Balladeer will speak again" : "Balladeer was not quiet"} in ${scope === "repo" ? `checkouts of ${key}` : key}.\n`);
|
|
509
|
+
if (earlier.length === 0) {
|
|
510
|
+
write("No earlier Balladeer is on this machine.\n");
|
|
458
511
|
return 0;
|
|
459
512
|
}
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
513
|
+
write("An earlier Balladeer is on this machine:\n");
|
|
514
|
+
for (const line of describeEarlier(earlier))
|
|
515
|
+
write(`${line}\n`);
|
|
516
|
+
const report = removeEarlier(earlier);
|
|
517
|
+
for (const line of describeRemoval(report))
|
|
518
|
+
write(`${line}\n`);
|
|
519
|
+
if (parsed.json)
|
|
520
|
+
write(`${JSON.stringify({ step: "earlier", removed: report.removed, failed: report.failed })}\n`);
|
|
521
|
+
return report.failed.length === 0 ? 0 : 6;
|
|
464
522
|
}
|
|
523
|
+
case "install":
|
|
524
|
+
return installEverything({ json: parsed.json, write, allowPartial: false });
|
|
465
525
|
case "setup":
|
|
466
526
|
return runSetup({
|
|
467
527
|
installCommand: () => {
|
|
468
|
-
const result =
|
|
469
|
-
environment: process.env,
|
|
470
|
-
json: parsed.json,
|
|
471
|
-
write,
|
|
472
|
-
allowPartial: true,
|
|
473
|
-
});
|
|
528
|
+
const result = installEverything({ json: parsed.json, write, allowPartial: true });
|
|
474
529
|
return process.platform === "win32" ? 0 : result;
|
|
475
530
|
},
|
|
476
531
|
controlPlane: parsed.controlPlane,
|
|
477
532
|
json: parsed.json,
|
|
478
533
|
wait: parsed.wait,
|
|
534
|
+
// A person is here when both ends of the terminal are a TTY; an agent
|
|
535
|
+
// shell has neither and keeps the print-and-return behaviour.
|
|
536
|
+
interactive: !parsed.noOpen &&
|
|
537
|
+
!parsed.json &&
|
|
538
|
+
process.stdout.isTTY === true &&
|
|
539
|
+
process.stdin.isTTY === true,
|
|
479
540
|
refresh: parsed.refresh,
|
|
480
541
|
chooseWorkspace: parsed.chooseWorkspace,
|
|
481
542
|
...(parsed.client === undefined ? {} : { client: parsed.client }),
|
|
@@ -2,7 +2,7 @@ import { readFileSync } from "node:fs";
|
|
|
2
2
|
import { callAgentTool, reportAgentEnforcementWarning, selectAgent, structuredString, } from "../agent.js";
|
|
3
3
|
import { batchProposalReviewLink, proposalReviewLink } from "../client.js";
|
|
4
4
|
import { commandLine } from "../release.js";
|
|
5
|
-
import { repositoryHint } from "../repository.js";
|
|
5
|
+
import { repositoryHint, repositoryHints } from "../repository.js";
|
|
6
6
|
import { StoreError, findSession, readCredentials } from "../store.js";
|
|
7
7
|
import { CLI_INVOCATION } from "../wire.js";
|
|
8
8
|
import { readTeachBackFile } from "./propose.js";
|
|
@@ -217,7 +217,7 @@ export async function runDiscover(options) {
|
|
|
217
217
|
catch (error) {
|
|
218
218
|
return fail(error instanceof StoreError ? error.code : "credential_store_unusable", error instanceof StoreError ? error.message : String(error), 4);
|
|
219
219
|
}
|
|
220
|
-
const selection = selectAgent(credentials.agents, options.controlPlane, options.repository, options.repo ??
|
|
220
|
+
const selection = selectAgent(credentials.agents, options.controlPlane, options.repository, options.repo ?? repositoryHints(options.cwd));
|
|
221
221
|
if (selection.kind === "refused") {
|
|
222
222
|
return fail("no_agent_connection", `${selection.reason} Run \`${commandLine(null, "setup")}\` in this repository first. Nothing was sent.`, 4);
|
|
223
223
|
}
|
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import { type GuidanceEvent } from "../guidance.js";
|
|
2
|
-
/**
|
|
2
|
+
/**
|
|
3
|
+
* What an agent, or `status`, is told about a folder Balladeer is not tracking.
|
|
4
|
+
*
|
|
5
|
+
* Robert, 22 September 2026: the session hook says nothing in such a folder.
|
|
6
|
+
* A customer with one repository has every other folder on the laptop
|
|
7
|
+
* untracked, and a line in each of those is noise in the wrong place. This
|
|
8
|
+
* sentence stays for the two readers who ask: `balladeer status` in the folder,
|
|
9
|
+
* and an agent that opens the empty catalog and wonders why.
|
|
10
|
+
*/
|
|
3
11
|
export declare function untrackedLine(remote: string): string;
|
|
4
12
|
export type GuidanceOptions = Readonly<{
|
|
5
13
|
controlPlane: string;
|
|
@@ -14,6 +22,8 @@ export type GuidanceOptions = Readonly<{
|
|
|
14
22
|
error?: (text: string) => void;
|
|
15
23
|
fetchImpl?: typeof fetch;
|
|
16
24
|
now?: () => number;
|
|
25
|
+
/** Runs the background update; injected so tests never reach npm. */
|
|
26
|
+
startUpdate?: (command: string, args: readonly string[]) => void;
|
|
17
27
|
}>;
|
|
18
28
|
/** Hook stdout is bounded context only. Host prompts and transcript paths never leave this process. */
|
|
19
29
|
export declare function runGuidance(options: GuidanceOptions): Promise<number>;
|
|
@@ -2,15 +2,22 @@ import { validateProjectGuidanceScope } from "../guidance-install.js";
|
|
|
2
2
|
import { GUIDANCE_UNAVAILABLE, loadGuidance, recordGuidanceContext, } from "../guidance.js";
|
|
3
3
|
import { readCredentials } from "../store.js";
|
|
4
4
|
import { selectAgent } from "../agent.js";
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
/**
|
|
5
|
+
import { repositoryHints } from "../repository.js";
|
|
6
|
+
import { newerVersionPublished } from "../currency.js";
|
|
7
|
+
import { startSelfUpdateIfDue } from "../self-update.js";
|
|
8
|
+
/**
|
|
9
|
+
* What an agent, or `status`, is told about a folder Balladeer is not tracking.
|
|
10
|
+
*
|
|
11
|
+
* Robert, 22 September 2026: the session hook says nothing in such a folder.
|
|
12
|
+
* A customer with one repository has every other folder on the laptop
|
|
13
|
+
* untracked, and a line in each of those is noise in the wrong place. This
|
|
14
|
+
* sentence stays for the two readers who ask: `balladeer status` in the folder,
|
|
15
|
+
* and an agent that opens the empty catalog and wonders why.
|
|
16
|
+
*/
|
|
9
17
|
export function untrackedLine(remote) {
|
|
10
18
|
const here = remote === "unknown/unknown" ? "This folder" : `This folder (${remote})`;
|
|
11
|
-
return (`${here} isn't
|
|
12
|
-
`To
|
|
13
|
-
`To stop this message here, run \`npx -y ${PUBLISHED_SPECIFIER} quiet\` (or \`quiet --repo\` for the whole repository).`);
|
|
19
|
+
return (`${here} isn't connected to Balladeer on this laptop, so nothing is tracked from here. ` +
|
|
20
|
+
`To connect it, run \`balladeer setup\` in this folder.`);
|
|
14
21
|
}
|
|
15
22
|
function readInput(stream) {
|
|
16
23
|
return new Promise((resolve, reject) => {
|
|
@@ -90,15 +97,13 @@ export async function runGuidance(options) {
|
|
|
90
97
|
? credentials.filter((agent) => agent.controlPlane === options.controlPlane &&
|
|
91
98
|
agent.repositoryId === options.repositoryId)
|
|
92
99
|
: (() => {
|
|
93
|
-
const selection = selectAgent(credentials, options.controlPlane, undefined,
|
|
100
|
+
const selection = selectAgent(credentials, options.controlPlane, undefined, repositoryHints(options.cwd));
|
|
94
101
|
return selection.kind === "refused" ? [] : [selection.agent];
|
|
95
102
|
})();
|
|
96
103
|
if (agents.length !== 1 || !agents[0]) {
|
|
97
104
|
if (projectScoped)
|
|
98
105
|
throw new Error("guidance_connection_unavailable");
|
|
99
|
-
// Untracked
|
|
100
|
-
if (event === "SessionStart" && !isQuiet(options.cwd, options.environment))
|
|
101
|
-
emit(untrackedLine(repositoryHint(options.cwd)));
|
|
106
|
+
// Untracked: nothing is said. `status` in the folder says why.
|
|
102
107
|
return 0;
|
|
103
108
|
}
|
|
104
109
|
const loaded = await loadGuidance({
|
|
@@ -107,6 +112,14 @@ export async function runGuidance(options) {
|
|
|
107
112
|
timeoutMs: 750,
|
|
108
113
|
...(options.fetchImpl ? { fetchImpl: options.fetchImpl } : {}),
|
|
109
114
|
});
|
|
115
|
+
// Behind? Start the install in the background, once a day at most, and
|
|
116
|
+
// carry on serving this session from the copy that is here.
|
|
117
|
+
if (event === "SessionStart")
|
|
118
|
+
startSelfUpdateIfDue(newerVersionPublished(), {
|
|
119
|
+
environment: options.environment,
|
|
120
|
+
...(options.startUpdate ? { run: options.startUpdate } : {}),
|
|
121
|
+
...(options.now ? { now: options.now } : {}),
|
|
122
|
+
});
|
|
110
123
|
const sessionId = identifier(record.session_id);
|
|
111
124
|
const agentId = identifier(record.agent_id);
|
|
112
125
|
recordGuidanceContext({
|
package/dist/commands/mcp.js
CHANGED
|
@@ -2,7 +2,7 @@ import { createInterface } from "node:readline";
|
|
|
2
2
|
import { installGuidanceLoader } from "../guidance-install.js";
|
|
3
3
|
import { callAgentTool, forwarderHeaders, noAgentCredentialSentence, safeJson, selectAgent, structuredString, updateLine, } from "../agent.js";
|
|
4
4
|
import { noteServerVersion, updateNotice } from "../currency.js";
|
|
5
|
-
import { repositoryHint } from "../repository.js";
|
|
5
|
+
import { repositoryHint, repositoryHints } from "../repository.js";
|
|
6
6
|
import { CLI_VERSION } from "../wire.js";
|
|
7
7
|
import { untrackedLine } from "./guidance.js";
|
|
8
8
|
import { StoreError, readCredentials } from "../store.js";
|
|
@@ -171,7 +171,7 @@ export async function runMcp(options) {
|
|
|
171
171
|
options.error(`${error instanceof StoreError ? error.message : String(error)}\n`);
|
|
172
172
|
return 4;
|
|
173
173
|
}
|
|
174
|
-
const selection = selectAgent(credentials.agents, options.controlPlane, options.repositoryId,
|
|
174
|
+
const selection = selectAgent(credentials.agents, options.controlPlane, options.repositoryId, repositoryHints(options.cwd));
|
|
175
175
|
if (selection.kind === "refused") {
|
|
176
176
|
// Issuing one in the browser is what this used to suggest here, and it is the
|
|
177
177
|
// move that produces this state: the bearer is shown once, there, and the
|
package/dist/commands/prepare.js
CHANGED
|
@@ -3,7 +3,7 @@ import { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
|
3
3
|
import { callAgentTool, noAgentCredentialSentence, reportAgentEnforcementWarning, selectAgent, } from "../agent.js";
|
|
4
4
|
import { inReadersZone } from "../local-time.js";
|
|
5
5
|
import { commandLine } from "../release.js";
|
|
6
|
-
import { repositoryHint } from "../repository.js";
|
|
6
|
+
import { repositoryHint, repositoryHints } from "../repository.js";
|
|
7
7
|
import { StoreError, readCredentials } from "../store.js";
|
|
8
8
|
import {} from "../wire.js";
|
|
9
9
|
const PROMISE_ID = /^prom_[a-z0-9]{8,64}$/;
|
|
@@ -138,7 +138,7 @@ export async function runPrepare(options) {
|
|
|
138
138
|
catch (error) {
|
|
139
139
|
return fail(error instanceof StoreError ? error.code : "credential_store_unusable", error instanceof StoreError ? error.message : String(error), 4);
|
|
140
140
|
}
|
|
141
|
-
const selection = selectAgent(credentials.agents, options.controlPlane, options.repository, options.repo ??
|
|
141
|
+
const selection = selectAgent(credentials.agents, options.controlPlane, options.repository, options.repo ?? repositoryHints(options.cwd));
|
|
142
142
|
if (selection.kind === "refused") {
|
|
143
143
|
if (selection.missingFor !== undefined) {
|
|
144
144
|
return fail("no_agent_credential_on_this_machine", `${noAgentCredentialSentence(selection.missingFor)} Nothing was prepared.`, 4);
|
package/dist/commands/propose.js
CHANGED
|
@@ -2,7 +2,7 @@ import { readFileSync } from "node:fs";
|
|
|
2
2
|
import { callAgentTool, noAgentCredentialSentence, reportAgentEnforcementWarning, selectAgent, structuredString, } from "../agent.js";
|
|
3
3
|
import { proposalReviewLink } from "../client.js";
|
|
4
4
|
import { commandLine } from "../release.js";
|
|
5
|
-
import { repositoryHint } from "../repository.js";
|
|
5
|
+
import { repositoryHint, repositoryHints } from "../repository.js";
|
|
6
6
|
import { StoreError, readCredentials } from "../store.js";
|
|
7
7
|
import { CLI_INVOCATION } from "../wire.js";
|
|
8
8
|
const MAX_FILE_BYTES = 64 * 1024;
|
|
@@ -215,7 +215,7 @@ export async function runPropose(options) {
|
|
|
215
215
|
catch (error) {
|
|
216
216
|
return fail(error instanceof StoreError ? error.code : "credential_store_unusable", error instanceof StoreError ? error.message : String(error), 4);
|
|
217
217
|
}
|
|
218
|
-
const selection = selectAgent(credentials.agents, options.controlPlane, options.repository, options.repo ??
|
|
218
|
+
const selection = selectAgent(credentials.agents, options.controlPlane, options.repository, options.repo ?? repositoryHints(options.cwd));
|
|
219
219
|
if (selection.kind === "refused") {
|
|
220
220
|
// The repository being connected somewhere else is not this machine holding
|
|
221
221
|
// a credential, and a proposal runs over the one this machine holds. Said in
|
package/dist/commands/session.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { callAgentTool, noAgentCredentialSentence, reportAgentEnforcementWarning, selectAgent, } from "../agent.js";
|
|
2
2
|
import { headCommit } from "../git.js";
|
|
3
|
-
import { repositoryHint } from "../repository.js";
|
|
3
|
+
import { repositoryHint, repositoryHints } from "../repository.js";
|
|
4
4
|
import { AGENT_SESSION_TRAILER, agentSessionTrailerLine, currentSession, readAgentSessionTrailer, } from "../session.js";
|
|
5
5
|
import { StoreError, readCredentials } from "../store.js";
|
|
6
6
|
import { CLI_INVOCATION } from "../wire.js";
|
|
@@ -43,7 +43,7 @@ export async function runSession(options) {
|
|
|
43
43
|
return fail(error instanceof StoreError ? error.code : "credential_store_unusable", error instanceof StoreError ? error.message : String(error), 4);
|
|
44
44
|
}
|
|
45
45
|
const here = options.repo ?? repositoryHint(options.cwd);
|
|
46
|
-
const selection = selectAgent(credentials.agents, options.controlPlane, options.repository,
|
|
46
|
+
const selection = selectAgent(credentials.agents, options.controlPlane, options.repository, options.repo ?? repositoryHints(options.cwd));
|
|
47
47
|
if (selection.kind === "refused") {
|
|
48
48
|
const message = selection.missingFor === undefined
|
|
49
49
|
? selection.reason
|
package/dist/commands/setup.d.ts
CHANGED
|
@@ -2,6 +2,15 @@ export type SetupOptions = Readonly<{
|
|
|
2
2
|
controlPlane: string;
|
|
3
3
|
json: boolean;
|
|
4
4
|
wait: boolean;
|
|
5
|
+
/**
|
|
6
|
+
* A person is at this terminal. The pairing link is opened in their browser
|
|
7
|
+
* and the run waits for the approval, so one command carries them through.
|
|
8
|
+
* Off in an agent shell, in `--json`, and under `--no-open`: those print the
|
|
9
|
+
* link and return. Robert, 22 September 2026.
|
|
10
|
+
*/
|
|
11
|
+
interactive?: boolean;
|
|
12
|
+
/** Opens a page for the person; injected so tests never reach a browser. */
|
|
13
|
+
openLink?: (url: string) => boolean;
|
|
5
14
|
repo?: string;
|
|
6
15
|
createWorkspace?: string;
|
|
7
16
|
client?: "codex" | "claude";
|
|
@@ -93,6 +102,12 @@ export declare function chooseRepositories(named: readonly string[], cwd: string
|
|
|
93
102
|
* a command that blocks for minutes and the pairing would be lost with it.
|
|
94
103
|
*/
|
|
95
104
|
export declare function runSetup(options: SetupOptions): Promise<number>;
|
|
105
|
+
/**
|
|
106
|
+
* The link, opened for a person at the terminal and for nobody else. Whether a
|
|
107
|
+
* browser appeared is not known here; the line beside the link says what to do
|
|
108
|
+
* if none did.
|
|
109
|
+
*/
|
|
110
|
+
export declare function openForPerson(options: SetupOptions, url: string): boolean;
|
|
96
111
|
/**
|
|
97
112
|
* Which invocation this repair writes, decided without asking anybody.
|
|
98
113
|
*
|