balladeer 1.0.12 → 1.0.14

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.
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Taking this product's own older, per-repository form out of a checkout once
3
+ * the user-scope form is in place, so one hook speaks per session.
4
+ *
5
+ * Releases before 1.0.8 registered Balladeer inside each repository: a hook in
6
+ * the checkout's `.claude/settings.json` (statusMessage "Balladeer current
7
+ * guidance (loader 1)"), a `balladeer` server in its `.mcp.json` that names the
8
+ * repository, and for Codex the fenced `# balladeer:guidance` and
9
+ * `# balladeer:mcp` blocks in `.codex/config.toml`. From 1.0.8 the same hook
10
+ * and server live at the host's user scope and run in every checkout. A laptop
11
+ * that moved from one to the other without cleaning up runs both: the session
12
+ * hears the guidance twice, and the server sees two hook forms fire for one
13
+ * person (found on Luke's laptop, 23 September 2026). Every removal here
14
+ * proves ownership from the entry itself; nothing else in those files moves.
15
+ * A file git tracks is edited without a copy (git holds the previous version)
16
+ * and the person is told to commit it; an untracked file is copied beside
17
+ * itself first.
18
+ */
19
+ export type CheckoutFormItem = Readonly<{
20
+ path: string;
21
+ found: string;
22
+ tracked: boolean;
23
+ remove: () => string | undefined;
24
+ }>;
25
+ export type CheckoutFormReport = Readonly<{
26
+ removed: readonly {
27
+ path: string;
28
+ found: string;
29
+ copy?: string;
30
+ tracked: boolean;
31
+ }[];
32
+ failed: readonly {
33
+ path: string;
34
+ found: string;
35
+ reason: string;
36
+ }[];
37
+ }>;
38
+ /** The older per-repository form in the checkout that holds `cwd`, if any. */
39
+ export declare function findCheckoutForm(input: {
40
+ cwd: string;
41
+ controlPlane: string;
42
+ now?: () => Date;
43
+ }): CheckoutFormItem[];
44
+ export declare function removeCheckoutForm(items: readonly CheckoutFormItem[]): CheckoutFormReport;
45
+ export declare function describeCheckoutFormRemoval(report: CheckoutFormReport): string[];
@@ -0,0 +1,223 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { copyFileSync, existsSync, lstatSync, readFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { isOurEntry, writeJsonAtomically } from "./mcp-config.js";
5
+ const PROJECT_OWNER = "Balladeer current guidance (loader 1)";
6
+ const CODEX_BLOCKS = [
7
+ ["# balladeer:guidance:start", "# balladeer:guidance:end"],
8
+ ["# balladeer:mcp:start", "# balladeer:mcp:end"],
9
+ ];
10
+ function repositoryRoot(cwd) {
11
+ try {
12
+ const root = execFileSync("git", ["rev-parse", "--show-toplevel"], {
13
+ cwd,
14
+ encoding: "utf8",
15
+ stdio: ["ignore", "pipe", "ignore"],
16
+ timeout: 2_000,
17
+ }).trim();
18
+ return root.length > 0 ? root : undefined;
19
+ }
20
+ catch {
21
+ return undefined;
22
+ }
23
+ }
24
+ function tracked(root, path) {
25
+ try {
26
+ execFileSync("git", ["ls-files", "--error-unmatch", "--", path], {
27
+ cwd: root,
28
+ stdio: "ignore",
29
+ timeout: 2_000,
30
+ });
31
+ return true;
32
+ }
33
+ catch {
34
+ return false;
35
+ }
36
+ }
37
+ function regularFile(path) {
38
+ try {
39
+ return lstatSync(path).isFile();
40
+ }
41
+ catch {
42
+ return false;
43
+ }
44
+ }
45
+ function readJson(path) {
46
+ try {
47
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
48
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed)
49
+ ? parsed
50
+ : undefined;
51
+ }
52
+ catch {
53
+ return undefined;
54
+ }
55
+ }
56
+ function backup(path, now) {
57
+ // A file name, not a sentence: the person reads the copy's path back, and the
58
+ // stamp only has to sort. Same form as the earlier-client backups.
59
+ const stamp = now().toISOString().replaceAll(/[:.]/g, "-");
60
+ const copy = `${path}.before-balladeer-${stamp}`;
61
+ copyFileSync(path, copy);
62
+ return copy;
63
+ }
64
+ const isProjectHook = (hook) => {
65
+ if (hook === null || typeof hook !== "object")
66
+ return false;
67
+ const { command, statusMessage } = hook;
68
+ if (statusMessage === PROJECT_OWNER)
69
+ return true;
70
+ return (typeof command === "string" &&
71
+ /balladeer/i.test(command) &&
72
+ /--hook (?:claude|codex)\b/.test(command) &&
73
+ /--repository\b/.test(command));
74
+ };
75
+ function claudeHooks(root, now) {
76
+ const path = join(root, ".claude", "settings.json");
77
+ if (!regularFile(path))
78
+ return [];
79
+ const settings = readJson(path);
80
+ const hooks = settings?.hooks;
81
+ if (!hooks || typeof hooks !== "object" || Array.isArray(hooks))
82
+ return [];
83
+ const events = Object.entries(hooks).filter(([, groups]) => Array.isArray(groups) &&
84
+ groups.some((group) => (group?.hooks ?? []).some((h) => isProjectHook(h))));
85
+ if (events.length === 0)
86
+ return [];
87
+ const isTracked = tracked(root, ".claude/settings.json");
88
+ return [
89
+ {
90
+ path,
91
+ tracked: isTracked,
92
+ found: `per-repository hook${events.length === 1 ? "" : "s"} (${events.map(([event]) => event).join(", ")})`,
93
+ remove: () => {
94
+ const copy = isTracked ? undefined : backup(path, now);
95
+ const current = readJson(path) ?? {};
96
+ const kept = {};
97
+ for (const [event, groups] of Object.entries((current.hooks ?? {}))) {
98
+ if (!Array.isArray(groups)) {
99
+ kept[event] = groups;
100
+ continue;
101
+ }
102
+ const left = groups.filter((group) => !(group?.hooks ?? []).some((h) => isProjectHook(h)));
103
+ if (left.length > 0)
104
+ kept[event] = left;
105
+ }
106
+ writeJsonAtomically(path, JSON.stringify({ ...current, hooks: kept }, null, 2) + "\n");
107
+ return copy;
108
+ },
109
+ },
110
+ ];
111
+ }
112
+ function claudeServer(root, controlPlane, now) {
113
+ const path = join(root, ".mcp.json");
114
+ if (!regularFile(path))
115
+ return [];
116
+ const config = readJson(path);
117
+ const servers = config?.mcpServers;
118
+ if (!servers || typeof servers !== "object" || Array.isArray(servers))
119
+ return [];
120
+ const entry = servers.balladeer;
121
+ if (entry === undefined || !isOurEntry(entry, controlPlane))
122
+ return [];
123
+ const args = entry.args;
124
+ if (!Array.isArray(args) || !args.includes("--repository"))
125
+ return [];
126
+ const isTracked = tracked(root, ".mcp.json");
127
+ return [
128
+ {
129
+ path,
130
+ tracked: isTracked,
131
+ found: "per-repository server entry",
132
+ remove: () => {
133
+ const copy = isTracked ? undefined : backup(path, now);
134
+ const current = readJson(path) ?? {};
135
+ const remaining = { ...(current.mcpServers ?? {}) };
136
+ delete remaining.balladeer;
137
+ writeJsonAtomically(path, JSON.stringify({ ...current, mcpServers: remaining }, null, 2) + "\n");
138
+ return copy;
139
+ },
140
+ },
141
+ ];
142
+ }
143
+ function codexBlocks(root, now) {
144
+ const path = join(root, ".codex", "config.toml");
145
+ if (!regularFile(path))
146
+ return [];
147
+ const text = readFileSync(path, "utf8");
148
+ const present = CODEX_BLOCKS.filter(([start, end]) => text.includes(`${start}\n`) && text.includes(end));
149
+ if (present.length === 0)
150
+ return [];
151
+ const isTracked = tracked(root, ".codex/config.toml");
152
+ return [
153
+ {
154
+ path,
155
+ tracked: isTracked,
156
+ found: `per-repository Codex block${present.length === 1 ? "" : "s"}`,
157
+ remove: () => {
158
+ const copy = isTracked ? undefined : backup(path, now);
159
+ let after = readFileSync(path, "utf8");
160
+ for (const [start, end] of CODEX_BLOCKS) {
161
+ const from = after.indexOf(`${start}\n`);
162
+ const to = after.indexOf(end, from);
163
+ if (from < 0 || to < 0)
164
+ continue;
165
+ const cut = to + end.length + (after[to + end.length] === "\n" ? 1 : 0);
166
+ after = after.slice(0, from) + after.slice(cut);
167
+ }
168
+ writeJsonAtomically(path, after.replace(/\n{3,}/g, "\n\n"));
169
+ return copy;
170
+ },
171
+ },
172
+ ];
173
+ }
174
+ /** The older per-repository form in the checkout that holds `cwd`, if any. */
175
+ export function findCheckoutForm(input) {
176
+ const root = repositoryRoot(input.cwd);
177
+ if (root === undefined || !existsSync(root))
178
+ return [];
179
+ const now = input.now ?? (() => new Date());
180
+ return [
181
+ ...claudeHooks(root, now),
182
+ ...claudeServer(root, input.controlPlane, now),
183
+ ...codexBlocks(root, now),
184
+ ];
185
+ }
186
+ export function removeCheckoutForm(items) {
187
+ const removed = [];
188
+ const failed = [];
189
+ for (const item of items) {
190
+ try {
191
+ const copy = item.remove();
192
+ removed.push({
193
+ path: item.path,
194
+ found: item.found,
195
+ tracked: item.tracked,
196
+ ...(copy === undefined ? {} : { copy }),
197
+ });
198
+ }
199
+ catch (error) {
200
+ failed.push({
201
+ path: item.path,
202
+ found: item.found,
203
+ reason: error instanceof Error ? error.message : String(error),
204
+ });
205
+ }
206
+ }
207
+ return { removed, failed };
208
+ }
209
+ export function describeCheckoutFormRemoval(report) {
210
+ const lines = [];
211
+ for (const entry of report.removed)
212
+ lines.push(`Removed the older ${entry.found} from ${entry.path}` +
213
+ (entry.copy
214
+ ? ` (copy at ${entry.copy}).`
215
+ : entry.tracked
216
+ ? "; git tracks this file, so commit the change."
217
+ : "."));
218
+ for (const entry of report.failed)
219
+ lines.push(`Could not remove the older ${entry.found} from ${entry.path}: ${entry.reason}`);
220
+ if (report.removed.length > 0)
221
+ lines.push("The user-scope hook now speaks alone in this checkout; it was saying the same thing twice.");
222
+ return lines;
223
+ }
package/dist/cli.js CHANGED
@@ -24,6 +24,7 @@ import { PUBLISHED_SPECIFIER } from "./release.js";
24
24
  import { installUserScope, runningFromCheckout, userHome, } from "./user-scope.js";
25
25
  import { updateNotice } from "./currency.js";
26
26
  import { describeEarlier, describeRemoval, findEarlier, removeEarlier } from "./remove-earlier.js";
27
+ import { describeCheckoutFormRemoval, findCheckoutForm, removeCheckoutForm, } from "./checkout-form.js";
27
28
  import { StoreError, normalizeControlPlane } from "./store.js";
28
29
  import { CLI_INVOCATION, CLI_VERSION, DEFAULT_CONTROL_PLANE } from "./wire.js";
29
30
  const USAGE = `balladeer ${CLI_VERSION}
@@ -471,6 +472,23 @@ function installEverything(options) {
471
472
  for (const w of writes ?? [])
472
473
  if (!options.json)
473
474
  options.write(`${w.status === "refused" ? "Not changed" : w.status === "written" ? "Registered" : "Already current"}: ${w.path}${w.reason ? ` (${w.reason})` : ""}\n`);
475
+ // With the user-scope form in place, the older per-repository form in this
476
+ // checkout only makes the session hear the guidance twice. Out it goes, with
477
+ // a copy beside any file git does not already keep.
478
+ if ((writes ?? []).length > 0 && !(writes ?? []).some((w) => w.status === "refused")) {
479
+ const checkoutForm = findCheckoutForm({
480
+ cwd: process.cwd(),
481
+ controlPlane: options.controlPlane,
482
+ });
483
+ if (checkoutForm.length > 0) {
484
+ const report = removeCheckoutForm(checkoutForm);
485
+ if (options.json)
486
+ options.write(`${JSON.stringify({ step: "checkout_form", removed: report.removed, failed: report.failed })}\n`);
487
+ else
488
+ for (const line of describeCheckoutFormRemoval(report))
489
+ options.write(`${line}\n`);
490
+ }
491
+ }
474
492
  if (!options.json && (writes ?? []).some((w) => w.status === "written"))
475
493
  options.write("Balladeer now runs in every Claude Code" +
476
494
  ((writes ?? []).some((w) => w.host === "codex") ? " and Codex" : "") +
@@ -522,11 +540,21 @@ async function dispatch(parsed, write) {
522
540
  return report.failed.length === 0 ? 0 : 6;
523
541
  }
524
542
  case "install":
525
- return installEverything({ json: parsed.json, write, allowPartial: false });
543
+ return installEverything({
544
+ json: parsed.json,
545
+ write,
546
+ allowPartial: false,
547
+ controlPlane: parsed.controlPlane,
548
+ });
526
549
  case "setup":
527
550
  return runSetup({
528
551
  installCommand: () => {
529
- const result = installEverything({ json: parsed.json, write, allowPartial: true });
552
+ const result = installEverything({
553
+ json: parsed.json,
554
+ write,
555
+ allowPartial: true,
556
+ controlPlane: parsed.controlPlane,
557
+ });
530
558
  return process.platform === "win32" ? 0 : result;
531
559
  },
532
560
  controlPlane: parsed.controlPlane,
@@ -1,13 +1,5 @@
1
1
  import { type GuidanceEvent } from "../guidance.js";
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
- */
2
+ /** A repository-specific setup step for explicit status and empty-catalog responses. */
11
3
  export declare function untrackedLine(remote: string): string;
12
4
  export type GuidanceOptions = Readonly<{
13
5
  controlPlane: string;
@@ -5,15 +5,7 @@ import { selectAgent } from "../agent.js";
5
5
  import { repositoryHints } from "../repository.js";
6
6
  import { newerVersionPublished } from "../currency.js";
7
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
- */
8
+ /** A repository-specific setup step for explicit status and empty-catalog responses. */
17
9
  export function untrackedLine(remote) {
18
10
  const here = remote === "unknown/unknown" ? "This folder" : `This folder (${remote})`;
19
11
  return (`${here} isn't connected to Balladeer on this laptop, so nothing is tracked from here. ` +
@@ -70,8 +62,7 @@ function identifier(input) {
70
62
  /** Hook stdout is bounded context only. Host prompts and transcript paths never leave this process. */
71
63
  export async function runGuidance(options) {
72
64
  // A project hook names its repository and must match the checkout it sits
73
- // in. A user-scope hook names none: the folder's remote decides, and a folder
74
- // with no connection hears that it is untracked rather than nothing at all.
65
+ // in. A user-scope hook names none: the folder's remotes decide.
75
66
  const projectScoped = options.repositoryId !== undefined;
76
67
  if (projectScoped &&
77
68
  !validateProjectGuidanceScope({
@@ -93,17 +84,18 @@ export async function runGuidance(options) {
93
84
  event = parsedEvent;
94
85
  const record = input;
95
86
  const credentials = readCredentials(options.environment).agents;
87
+ const repositories = projectScoped ? [] : repositoryHints(options.cwd);
96
88
  const agents = projectScoped
97
89
  ? credentials.filter((agent) => agent.controlPlane === options.controlPlane &&
98
90
  agent.repositoryId === options.repositoryId)
99
91
  : (() => {
100
- const selection = selectAgent(credentials, options.controlPlane, undefined, repositoryHints(options.cwd));
92
+ const selection = selectAgent(credentials, options.controlPlane, undefined, repositories);
101
93
  return selection.kind === "refused" ? [] : [selection.agent];
102
94
  })();
103
95
  if (agents.length !== 1 || !agents[0]) {
104
96
  if (projectScoped)
105
97
  throw new Error("guidance_connection_unavailable");
106
- // Untracked: nothing is said. `status` in the folder says why.
98
+ // Unconnected folders stay silent. An explicit status request explains setup.
107
99
  return 0;
108
100
  }
109
101
  const loaded = await loadGuidance({
@@ -21,7 +21,7 @@ import { StoreError, assertStoreWritable, credentialsPath, dropPendingPairing, f
21
21
  import { CLI_VERSION, DELEGATED_SCOPES, } from "../wire.js";
22
22
  import { explainText } from "./explain.js";
23
23
  import { probeAgentConnection } from "./mcp.js";
24
- const REFUSAL_SENTENCE = "It may not agree to what a promise means, agree a change to what a promise means, grant an exception, transfer ownership, supersede, retire, or offboard anything.";
24
+ const REFUSAL_SENTENCE = "It may not agree to what a promise means, agree a change to what a promise means, transfer ownership, supersede, retire, or offboard anything.";
25
25
  const MAX_WAIT_MS = 10 * 60 * 1000;
26
26
  const AGENT_WAIT_MS = 90 * 1000;
27
27
  /** The bound the control plane's own name check holds, refused here so a name
@@ -2027,7 +2027,7 @@ var require_toml = __commonJS({
2027
2027
  });
2028
2028
 
2029
2029
  // src/wire.ts
2030
- var CLI_VERSION = "1.0.12";
2030
+ var CLI_VERSION = "1.0.14";
2031
2031
  var CLIENT_HEADER = "x-balladeer-client";
2032
2032
  var CLIENT_HEADER_VALUE = `balladeer/${CLI_VERSION}`;
2033
2033
  var DEFAULT_CONTROL_PLANE = "https://envelopes.balladeer.ai";
@@ -3107,15 +3107,11 @@ async function runGuidance(options) {
3107
3107
  event = parsedEvent;
3108
3108
  const record = input;
3109
3109
  const credentials = readCredentials(options.environment).agents;
3110
+ const repositories = projectScoped ? [] : repositoryHints(options.cwd);
3110
3111
  const agents = projectScoped ? credentials.filter(
3111
3112
  (agent) => agent.controlPlane === options.controlPlane && agent.repositoryId === options.repositoryId
3112
3113
  ) : (() => {
3113
- const selection = selectAgent(
3114
- credentials,
3115
- options.controlPlane,
3116
- void 0,
3117
- repositoryHints(options.cwd)
3118
- );
3114
+ const selection = selectAgent(credentials, options.controlPlane, void 0, repositories);
3119
3115
  return selection.kind === "refused" ? [] : [selection.agent];
3120
3116
  })();
3121
3117
  if (agents.length !== 1 || !agents[0]) {
@@ -94,8 +94,15 @@ function claudeConnector(home, controlPlane, now) {
94
94
  },
95
95
  ];
96
96
  }
97
+ // This product's own hook runs `guidance --hook claude|codex --event ...`,
98
+ // whether by npx, by a checkout path, or by the installed command's absolute
99
+ // path (1.0.11 and later). The July client's hook ran nothing of the kind, so
100
+ // that shape is the line: a hook that names it is ours and is never "earlier".
101
+ // Found on 22 September 2026: the installed-path form was being removed as
102
+ // the older client on every setup run, leaving the laptop with no hook.
97
103
  const isEarlierHook = (command) => typeof command === "string" &&
98
104
  /balladeer/i.test(command) &&
105
+ !/\bguidance --hook (?:claude|codex)\b/.test(command) &&
99
106
  !/balladeer@(?:latest|\d+\.\d+\.\d+)\b/.test(command) &&
100
107
  !/\bpackages[/\\]cli[/\\]dist[/\\]cli\.js\b/.test(command);
101
108
  function claudeSettings(home, now) {
@@ -15,6 +15,8 @@
15
15
  * shape, never by trust in the name.
16
16
  */
17
17
  export declare const USER_SCOPE_OWNER = "Balladeer current guidance (user scope, loader 1)";
18
+ /** Every tool of the server named `balladeer`, in Claude Code's permission rule form. */
19
+ export declare const USER_SCOPE_ALLOW_RULE = "mcp__balladeer__*";
18
20
  export type UserScopeHost = "claude" | "codex";
19
21
  export type UserScopeWrite = Readonly<{
20
22
  host: UserScopeHost;
@@ -22,6 +22,8 @@ import { writeJsonAtomically } from "./mcp-config.js";
22
22
  */
23
23
  export const USER_SCOPE_OWNER = "Balladeer current guidance (user scope, loader 1)";
24
24
  const CLAUDE_EVENTS = ["SessionStart", "UserPromptSubmit", "SubagentStart"];
25
+ /** Every tool of the server named `balladeer`, in Claude Code's permission rule form. */
26
+ export const USER_SCOPE_ALLOW_RULE = "mcp__balladeer__*";
25
27
  /**
26
28
  * The command a host runs to reach Balladeer, with no repository named: the
27
29
  * directory decides.
@@ -192,6 +194,24 @@ export function mergeClaudeUserHooks(home, published, installed) {
192
194
  hooks[event] = [...rows.filter((row) => !own.includes(row)), expected];
193
195
  changed = true;
194
196
  }
197
+ // Claude Code asks the person before every call to a server's tools unless
198
+ // a rule allows them. Without one, each read the guidance asks for costs a
199
+ // click, and a session that says no once hears no offer at all (measured
200
+ // 23 September 2026). The rule names this server's tools and nothing else;
201
+ // a deny rule the person wrote for them is theirs and is left to win.
202
+ const permissions = root.permissions && typeof root.permissions === "object" && !Array.isArray(root.permissions)
203
+ ? { ...root.permissions }
204
+ : {};
205
+ const allow = permissions.allow === undefined ? [] : permissions.allow;
206
+ if (!Array.isArray(allow))
207
+ return { host, path, status: "refused", reason: "permissions.allow is not a list" };
208
+ const deny = Array.isArray(permissions.deny) ? permissions.deny : [];
209
+ const denied = deny.some((rule) => typeof rule === "string" && /^mcp__balladeer(__|$)/.test(rule));
210
+ if (!denied && !allow.some((rule) => rule === USER_SCOPE_ALLOW_RULE)) {
211
+ permissions.allow = [...allow, USER_SCOPE_ALLOW_RULE];
212
+ root.permissions = permissions;
213
+ changed = true;
214
+ }
195
215
  if (!changed)
196
216
  return { host, path, status: "current" };
197
217
  root.hooks = hooks;
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.12";
8
+ export declare const CLI_VERSION = "1.0.14";
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.12";
11
+ export declare const CLIENT_HEADER_VALUE = "balladeer/1.0.14";
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[];
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.12";
8
+ export const CLI_VERSION = "1.0.14";
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.12",
3
+ "version": "1.0.14",
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,