@skanl/brambo-cli 0.1.1
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/LICENSE +21 -0
- package/README.md +208 -0
- package/dist/bin/brambo.d.ts +2 -0
- package/dist/bin/brambo.js +9 -0
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.js +1 -0
- package/dist/src/registry-commands.d.ts +94 -0
- package/dist/src/registry-commands.js +612 -0
- package/dist/src/run.d.ts +26 -0
- package/dist/src/run.js +1157 -0
- package/dist/src/swap-command.d.ts +28 -0
- package/dist/src/swap-command.js +225 -0
- package/package.json +59 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 SKANL
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
# @skanl/brambo-cli
|
|
2
|
+
|
|
3
|
+
Minimal `brambo` command-line surface, and nothing more than a binding: it parses
|
|
4
|
+
argv, calls [`@skanl/brambo-session`](../session/README.md), prints the typed envelope as
|
|
5
|
+
structured JSON and maps it to an exit code. The composition it used to own —
|
|
6
|
+
workspace under `<cwd>/.brambo/workspaces/<uuid>`, adapter, cancellation, cleanup —
|
|
7
|
+
lives in that package, so a third party can do everything this CLI does without
|
|
8
|
+
installing it. It reads no files itself; `eslint.config.js` forbids this package
|
|
9
|
+
from importing `node:fs` at all, because a capability that needs a filesystem
|
|
10
|
+
read is a capability that belongs in `@skanl/brambo-session` or `@skanl/brambo-environment`.
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
```sh
|
|
15
|
+
npm i -g @skanl/brambo-cli
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
The binary is `brambo`. Every command below is written the way it works after
|
|
19
|
+
that install; inside this repository's own checkout the same commands run as
|
|
20
|
+
`pnpm brambo …`, which is a development convenience and not what a consumer types.
|
|
21
|
+
|
|
22
|
+
## Usage
|
|
23
|
+
|
|
24
|
+
```sh
|
|
25
|
+
brambo run "list files in this workspace"
|
|
26
|
+
brambo run --executor codex "list files in this workspace"
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Output: the `ResultEnvelope` as pretty-printed JSON on stdout.
|
|
30
|
+
|
|
31
|
+
## Choosing an executor
|
|
32
|
+
|
|
33
|
+
Brambo ships three adapters — `claude-code`, `codex` and `opencode` — and resolves
|
|
34
|
+
which one runs through layered configuration, widest to narrowest:
|
|
35
|
+
|
|
36
|
+
| Layer | Source |
|
|
37
|
+
| ----- | ------ |
|
|
38
|
+
| `defaults` | brambo's built-in default, `claude-code` |
|
|
39
|
+
| `global` | `~/.brambo/config.json` |
|
|
40
|
+
| `project` | `<project>/.brambo/config.json` |
|
|
41
|
+
| `invocation` | `--executor <id>` (or `--executor=<id>`) |
|
|
42
|
+
|
|
43
|
+
The document is JSON. `executor` is the key this table resolves; the same file
|
|
44
|
+
carries the other selections brambo reads — `method` (see `brambo swap method`) and
|
|
45
|
+
the `workspace` subtree that `@skanl/brambo-workspace-local` and
|
|
46
|
+
`@skanl/brambo-workspace-git-worktree` document:
|
|
47
|
+
|
|
48
|
+
```json
|
|
49
|
+
{ "executor": "codex" }
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
**Every real invocation now writes one line to stderr**, naming the selection and
|
|
53
|
+
the layer that decided it — `executor: codex (selected by the 'project' layer)`.
|
|
54
|
+
A run whose output cannot tell you which agent produced it is not a swap you can
|
|
55
|
+
trust. This is a behaviour change for a script that treats any stderr output as
|
|
56
|
+
failure; branch on the exit code instead.
|
|
57
|
+
|
|
58
|
+
A configuration document that is MISSING is simply an absent layer. One that
|
|
59
|
+
exists and cannot be used — unreadable, a dangling symlink, invalid JSON, not an
|
|
60
|
+
object, or an `executor` that is not a string — is a coded error and exits 2. It
|
|
61
|
+
is never a quiet fall back to the default, because running a different agent than
|
|
62
|
+
the one you configured is the failure this feature exists to remove. `--executor`
|
|
63
|
+
overrides a configuration brambo can READ; it does not rescue one it cannot.
|
|
64
|
+
|
|
65
|
+
## Putting something in the registry
|
|
66
|
+
|
|
67
|
+
Everything `brambo init` projects comes out of the registry, and until story M4.D
|
|
68
|
+
there was no way to put anything in it from the binary — the store shipped, the
|
|
69
|
+
surface did not.
|
|
70
|
+
|
|
71
|
+
```sh
|
|
72
|
+
brambo add skill commit-lint --entry-path ./skills/commit-lint/SKILL.md
|
|
73
|
+
brambo add mcp-server fs --command npx --arg -y --arg @modelcontextprotocol/server-filesystem
|
|
74
|
+
brambo list
|
|
75
|
+
brambo remove skill commit-lint
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
The scope comes from the GRAMMAR, never from a flag: `brambo <verb>` is the
|
|
79
|
+
machine scope and `brambo project <verb> … [directory]` is a project's, exactly
|
|
80
|
+
like `init`, `doctor` and `remediate`. There is deliberately no `--scope agent` —
|
|
81
|
+
the agent scope is an in-memory map that dies with the process, so a flag for it
|
|
82
|
+
would accept the flag, exit 0 and persist nothing.
|
|
83
|
+
|
|
84
|
+
Which field flags a type accepts is the registry CONTRACT's answer, not this
|
|
85
|
+
command's: `brambo add mcp-server t --entry-path ./x` is refused with
|
|
86
|
+
`BRAMBO_REGISTRY_INVALID_ENTRY`, because an `mcp-server` carries a `command` and
|
|
87
|
+
`args`. The CLI holds no per-type table, so it cannot drift from the one the
|
|
88
|
+
contract already has.
|
|
89
|
+
|
|
90
|
+
Story M4.E RETIRED the `tool` type: no executor has a non-MCP location for an
|
|
91
|
+
identity plus an executable command, and an `mcp-server` entry already carries
|
|
92
|
+
exactly what a `tool` entry carried. Story M4.F retired `profile` through the
|
|
93
|
+
same door, on the PRD's own glossary: a Profile is a named bundle of registry
|
|
94
|
+
SELECTIONS carried by Bundles, so it is a container over `skill` and
|
|
95
|
+
`mcp-server` rather than a peer of them, and it returns designed in Epic 5.
|
|
96
|
+
`brambo add tool …` and `brambo add profile …` are usage errors, but a registry
|
|
97
|
+
written by an older build is still read, still listed, and still emptied
|
|
98
|
+
through the product — `brambo remove` accepts a retired type, and `brambo doctor`
|
|
99
|
+
prints the exact spelling for each entry it finds.
|
|
100
|
+
|
|
101
|
+
`add` registers and projects nothing; it names the command that does. Coupling
|
|
102
|
+
the two would make registration fail for projection reasons.
|
|
103
|
+
|
|
104
|
+
That next step is DERIVED from the same planner `brambo init` runs, never written
|
|
105
|
+
beside the command, and it can say that nothing takes the entry at all:
|
|
106
|
+
|
|
107
|
+
```
|
|
108
|
+
$ brambo project add skill deadend --entry-path ./s.md
|
|
109
|
+
registered: project - skill - deadend
|
|
110
|
+
NOTHING TAKES IT HERE: no detected executor has a project-scope location for a
|
|
111
|
+
skill entry, so `brambo project init` would project it nowhere
|
|
112
|
+
the machine scope takes it (codex): register it with `brambo add` and project it
|
|
113
|
+
with `brambo init`
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
That is a real dead end and it used to be silent: no executor has a
|
|
117
|
+
project-scope skills root brambo has verified, machine-scope projection cannot
|
|
118
|
+
see a project-scope entry, and `add` still pointed at `brambo project init`, which
|
|
119
|
+
exits 0 and delivers nothing. The registration still succeeds — the entry is
|
|
120
|
+
yours and `brambo list` shows it — but the command you are pointed at is the one
|
|
121
|
+
that would actually deliver it, or none, and never a command that would not.
|
|
122
|
+
|
|
123
|
+
`remove` on an entry that was not there says so and exits 1. An empty `list`
|
|
124
|
+
exits 0 — an empty list is a result, not a failure.
|
|
125
|
+
|
|
126
|
+
## Leaving a state `brambo doctor` reported
|
|
127
|
+
|
|
128
|
+
`brambo doctor` reports drift and refuses to resolve it, which is correct and used
|
|
129
|
+
to be terminal: the only exit was hand-editing `~/.brambo/projection-ledger.json`,
|
|
130
|
+
the file every safety guarantee in that subsystem is stored in.
|
|
131
|
+
|
|
132
|
+
```sh
|
|
133
|
+
brambo remediate adopt --executor claude-code --entry context7 # describes
|
|
134
|
+
brambo remediate adopt --executor claude-code --entry context7 --apply # performs
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
One finding at a time, named by the user, and only a finding the same run just
|
|
138
|
+
reported — zero matches and more than one match are both refusals, and the
|
|
139
|
+
refusal lists what could have been named. Without `--apply` the command only
|
|
140
|
+
describes, computed by the code that would perform it. Nothing is ever
|
|
141
|
+
remediated automatically or in bulk.
|
|
142
|
+
|
|
143
|
+
| Verb | What it changes | What it lets a LATER run do |
|
|
144
|
+
| ---- | --------------- | --------------------------- |
|
|
145
|
+
| `adopt` | brambo's ledger only | brambo OWNS the location: the next `brambo init` replaces what is there, and on a skills root can remove it. Every path that becomes deletable is named in the description first |
|
|
146
|
+
| `release` | brambo's ledger only | nothing — the claim goes, the file is not even opened, and no later run touches it |
|
|
147
|
+
| `repair` | brambo's own ledger document, dropping exactly the records it cannot read | nothing to a vendor file, ever |
|
|
148
|
+
| `discard` | one vendor file, removing only brambo's own prior output (correction-01 C6) | nothing further |
|
|
149
|
+
|
|
150
|
+
Three of the four write nothing but brambo's own ledger — but `adopt` is an
|
|
151
|
+
ownership TRANSFER, and owning a location is what makes it replaceable and, on a
|
|
152
|
+
skills root, removable. Read the description: it says which of the two the next
|
|
153
|
+
`brambo init` will do and which paths it covers. A `remediate` that brambo refuses
|
|
154
|
+
exits 1 with `BRAMBO_PROJECTION_REMEDIATION_REFUSED` and changes nothing.
|
|
155
|
+
|
|
156
|
+
`brambo doctor` names the verb for every state that has one, so the report and the
|
|
157
|
+
exit are one string.
|
|
158
|
+
|
|
159
|
+
## How much quota is left
|
|
160
|
+
|
|
161
|
+
```sh
|
|
162
|
+
brambo status
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
One row per executor, reporting what that executor published about its own usage
|
|
166
|
+
the last time brambo ran it — the windows the vendor NAMES, with the vendor's own
|
|
167
|
+
utilisation and reset values and the instant the reading was taken. Brambo
|
|
168
|
+
averages nothing, converts nothing, and states no "time remaining" the vendor did
|
|
169
|
+
not state.
|
|
170
|
+
|
|
171
|
+
`brambo status` **invokes no executor and writes nothing**. A report that spent
|
|
172
|
+
the quota it reports on would be unusable on exactly the day you most want it, so
|
|
173
|
+
the run that already paid for a reading is the one that records it, and `status`
|
|
174
|
+
reads the record. That is also why every row is honest about what it does not
|
|
175
|
+
know: an executor that publishes no usage surface says so
|
|
176
|
+
(`BRAMBO_USAGE_NO_SURFACE`), and one brambo has not run yet says so and names the
|
|
177
|
+
command that would produce a reading (`BRAMBO_USAGE_NOT_OBSERVED`). Neither is
|
|
178
|
+
ever shown as `0%` — a zero for something brambo never measured reads as a
|
|
179
|
+
measurement that was taken.
|
|
180
|
+
|
|
181
|
+
Today only `claude-code` publishes such a surface, in its own event stream. It is
|
|
182
|
+
a typed field the vendor emits deliberately under a documented flag, not text
|
|
183
|
+
scraped off a terminal.
|
|
184
|
+
|
|
185
|
+
## Exit codes
|
|
186
|
+
|
|
187
|
+
| Code | Meaning |
|
|
188
|
+
| ---- | ------- |
|
|
189
|
+
| 0 | run completed with a status `ok` envelope |
|
|
190
|
+
| 1 | run returned `failed` or `cancelled` (envelope still printed) |
|
|
191
|
+
| 2 | usage error, invalid request, or environment failure (message on stderr) |
|
|
192
|
+
|
|
193
|
+
For `status` there are two: 0 whenever a report could be produced — an
|
|
194
|
+
all-absence report is still a report — and 2 only when none could be. There is no
|
|
195
|
+
1, because a utilisation is not a verdict brambo gets to fail on.
|
|
196
|
+
|
|
197
|
+
For `doctor` the three narrow: 0 clean, 1 at least one finding that is a problem,
|
|
198
|
+
2 no diagnosis could be produced. For `remediate`: 0 described or performed, 1
|
|
199
|
+
brambo refused, 2 usage or environment failure. For `remove`: 0 removed, 1 the
|
|
200
|
+
entry was not registered at that scope (typed absence, never a silent 0), 2 usage
|
|
201
|
+
or a coded registry failure.
|
|
202
|
+
|
|
203
|
+
An executor name brambo has no adapter for (`BRAMBO_EXECUTOR_NOT_FOUND`) and an
|
|
204
|
+
unusable configuration document (`BRAMBO_CONFIGURATION_UNUSABLE`) are both 2, and
|
|
205
|
+
carry distinct codes because their fixes differ: correct the name versus repair
|
|
206
|
+
the file.
|
|
207
|
+
|
|
208
|
+
No TUI, no streaming — progress surfaces arrive in later stories.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { runBrambo } from '../src/run.js';
|
|
3
|
+
// `brambo run ... | head` must not crash: a closed stdout pipe surfaces as an
|
|
4
|
+
// async EPIPE error on the stream, which we treat as a graceful exit.
|
|
5
|
+
process.stdout?.on('error', (error) => {
|
|
6
|
+
if (error.code === 'EPIPE')
|
|
7
|
+
process.exit(0);
|
|
8
|
+
});
|
|
9
|
+
process.exitCode = await runBrambo(process.argv.slice(2));
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { runBrambo } from './run.js';
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import type { OmittedEntry } from '@skanl/brambo-environment';
|
|
2
|
+
export type RegistryVerb = 'add' | 'remove' | 'list';
|
|
3
|
+
/** The verbs `run.ts` dispatches into this file, under both grammars. */
|
|
4
|
+
export declare const REGISTRY_VERBS: readonly RegistryVerb[];
|
|
5
|
+
export declare function isRegistryVerb(token: string | undefined): token is RegistryVerb;
|
|
6
|
+
export interface RegistryCommandContext {
|
|
7
|
+
readonly out: (line: string) => void;
|
|
8
|
+
readonly err: (line: string) => void;
|
|
9
|
+
/** The synopsis printed after a usage error; `run.ts` owns the text. */
|
|
10
|
+
readonly defaultUsage: string;
|
|
11
|
+
/** Defaults to the OS home directory, like every other command's. */
|
|
12
|
+
readonly homeDir: string | undefined;
|
|
13
|
+
/** The project directory when no positional one is given. */
|
|
14
|
+
readonly cwd: string | undefined;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* The entry type, as an argv question. A missing or misspelled type is a usage
|
|
18
|
+
* error about the command line — the user has not named an entry yet — while
|
|
19
|
+
* everything about the entry ITSELF is the contract's to answer.
|
|
20
|
+
*/
|
|
21
|
+
/**
|
|
22
|
+
* The verb a user at THIS scope can actually run.
|
|
23
|
+
*
|
|
24
|
+
* `brambo project add` with no type answered `brambo add needs an entry type`. The
|
|
25
|
+
* verb was right and the grammar was not: at project scope `brambo add` is a
|
|
26
|
+
* different command against a different registry, so the sentence named
|
|
27
|
+
* something that would act on the wrong one. `scope` was already a parameter at
|
|
28
|
+
* every one of these sites and was simply not read.
|
|
29
|
+
*
|
|
30
|
+
* Two spellings rather than one interpolation, so the printed-command invariant
|
|
31
|
+
* sees a real verb in each — the same shape `doctor.ts` uses for its exits.
|
|
32
|
+
*/
|
|
33
|
+
export declare function verbAt(scope: 'machine' | 'project', verb: string): string;
|
|
34
|
+
export declare function runRegistryCommand(verb: RegistryVerb, tokens: readonly string[], scope: 'machine' | 'project', context: RegistryCommandContext): Promise<number>;
|
|
35
|
+
/**
|
|
36
|
+
* `brambo export <path>` — the machine's Registry as a portable artifact.
|
|
37
|
+
*
|
|
38
|
+
* It lives beside `add`/`remove`/`list` rather than in its own module because
|
|
39
|
+
* it needs `bind`, and `bind` is the trust boundary those three already share:
|
|
40
|
+
* one spelling of "the home directory" and a bound directory brambo never
|
|
41
|
+
* creates. A second binding here is how two verbs come to disagree about which
|
|
42
|
+
* registry they are talking about.
|
|
43
|
+
*
|
|
44
|
+
* It is NOT a RegistryVerb. Those three also carry a project-scoped spelling,
|
|
45
|
+
* and a project-scoped export would name a directory the destination machine
|
|
46
|
+
* does not have — so the global scope is the only one that can travel, and
|
|
47
|
+
* there is no second grammar to offer.
|
|
48
|
+
*/
|
|
49
|
+
export declare function runExportCommand(tokens: readonly string[], context: RegistryCommandContext): Promise<number>;
|
|
50
|
+
/** What an import put in place, before the projection half runs. */
|
|
51
|
+
export interface ImportInstallation {
|
|
52
|
+
readonly path: string;
|
|
53
|
+
readonly homeDir: string;
|
|
54
|
+
readonly imported: number;
|
|
55
|
+
/** Entries whose `type:id` was already registered here and has been taken over. */
|
|
56
|
+
readonly replaced: readonly {
|
|
57
|
+
type: string;
|
|
58
|
+
id: string;
|
|
59
|
+
}[];
|
|
60
|
+
/**
|
|
61
|
+
* What the bundle could not carry, forwarded verbatim: FR-22's manual work.
|
|
62
|
+
*
|
|
63
|
+
* `OmittedEntry` itself rather than a structural copy of its shape. The copy
|
|
64
|
+
* spelled `{type, id, field}`, which is exactly the shape the `id` arm must
|
|
65
|
+
* NOT have — so a hand-written mirror of a type is how a second slot for the
|
|
66
|
+
* credential gets re-opened one package downstream.
|
|
67
|
+
*/
|
|
68
|
+
readonly pending: readonly OmittedEntry[];
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* `brambo import <path>` — the install half. The caller re-projects.
|
|
72
|
+
*
|
|
73
|
+
* Split there on purpose: re-projection is `initMachine`, whose result already
|
|
74
|
+
* has one reporting implementation in `run.ts`, and this file may not grow a
|
|
75
|
+
* second one. What belongs here is the part that needs `bind` — the same trust
|
|
76
|
+
* boundary add/remove/list/export share.
|
|
77
|
+
*/
|
|
78
|
+
export declare function runImportCommand(tokens: readonly string[], context: RegistryCommandContext): Promise<ImportInstallation | number>;
|
|
79
|
+
/**
|
|
80
|
+
* `brambo ingest [--dry-run]` — the machine's own skills AND MCP servers, into
|
|
81
|
+
* the registry.
|
|
82
|
+
*
|
|
83
|
+
* The binding's whole job, and the reason it is this short: argv in, one
|
|
84
|
+
* capability call, the outcome rendered, exit 0. Which skills roots and which
|
|
85
|
+
* executor configs are read, what the ownership ledger excludes, and what counts
|
|
86
|
+
* as a skill or a server are `ingestMachine`'s answers — a second opinion here
|
|
87
|
+
* would be a rule that drifts from the one the capability enforces, and the CLI
|
|
88
|
+
* may not touch the filesystem at all.
|
|
89
|
+
*
|
|
90
|
+
* A run that wrote nothing because there was nothing to write exits 0. That is
|
|
91
|
+
* the same answer `brambo list` gives for an empty registry: it is a result, not
|
|
92
|
+
* a failure, and a script must be able to tell it apart from a run that broke.
|
|
93
|
+
*/
|
|
94
|
+
export declare function runIngestCommand(tokens: readonly string[], context: RegistryCommandContext): Promise<number>;
|