@bugb-technologies/bravos 1.0.2 → 1.0.3

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/cli.js CHANGED
@@ -11,7 +11,7 @@ import { writeOmpExtension } from './omp.js';
11
11
  import { homeOf, installGlobal } from './global.js';
12
12
  import { uninstall } from './uninstall.js';
13
13
  import { checkKind, newToken, readAllAnswers, readDraft, renderReview, summarize, writePage } from './review.js';
14
- import { spawn as spawnChild } from 'node:child_process';
14
+ import { spawn as spawnChild, spawnSync } from 'node:child_process';
15
15
  import { createServer as netServer } from 'node:net';
16
16
  import { dirname as pathDirname, join as pathJoin } from 'node:path';
17
17
  import { OPENCODE_CONFIG, writeOpencode } from './opencode.js';
@@ -485,6 +485,23 @@ export function buildProgram(io, result) {
485
485
  }
486
486
  lines.push(JSON.stringify(opts.json ? a : a.result, null, 2));
487
487
  }));
488
+ // @comment -- "bravos hook <name> runs one of the hook scripts this install ships (session-start, pre-pr, autopilot) with the given arguments, stdin passed through: what a plugin's skills call where the repository has no .bravos/hooks/ copy. Only those three names run; the script's own exit code is returned."
489
+ program
490
+ .command('hook <name> [args...]')
491
+ .description('Run one of bravos\'s hook scripts from this install (session-start, pre-pr, autopilot), e.g. `bravos hook session-start --briefing` or `bravos hook pre-pr --check`: what plugin installs use where the repository has no .bravos/hooks/.')
492
+ .allowUnknownOption()
493
+ .helpOption(false)
494
+ .action((name, args) => guarded((lines) => {
495
+ const known = ['session-start', 'pre-pr', 'autopilot'];
496
+ if (!known.includes(name))
497
+ throw new BravosError(`unknown hook '${name}'; use ${known.join(', ')}`);
498
+ const r = spawnSync(process.execPath, [pathJoin(hooksDir(), `${name}.mjs`), ...args], { env: io.env, encoding: 'utf8', stdio: ['inherit', 'pipe', 'pipe'], maxBuffer: 16 * 1024 * 1024 });
499
+ if (r.stdout)
500
+ lines.push(r.stdout.replace(/\n$/, ''));
501
+ if (r.stderr)
502
+ io.stderr(r.stderr.replace(/\n$/, ''));
503
+ result.code = r.status ?? 1;
504
+ }));
488
505
  program
489
506
  .command('doctor [dir]')
490
507
  .description('Read-only check of the tools, licence, skills and hooks bravos relies on.')
@@ -0,0 +1,212 @@
1
+ import { mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
2
+ import { dirname, join, relative } from 'node:path';
3
+ import { SERVERS } from './mcp.js';
4
+ import { hooksDir, packageRoot, version } from './paths.js';
5
+ import { listBundledSkills, renderPluginSkillFile, renderPortableSkillFile, viaCliHooks } from './skills.js';
6
+ // @comment -- "The public plugin repository (Bugb-Technologies/bravos-plugins): one generated tree that every harness installs from. The repository root is the Gemini CLI extension and the Antigravity plugin; claude/, codex/, copilot/ and cursor/ are the other harnesses' plugins, each listed by that harness's own marketplace file at the root. It carries the skills, the session hook scripts and the guardlink MCP entry, never the CLI's source, the analyzer or the code graph, and the Bravos licence."
7
+ export const RELEASE_REPO = 'Bugb-Technologies/bravos-plugins';
8
+ const NAME = 'bravos';
9
+ const DESCRIPTION = 'Bravos makes your coding agent work the threat model (guardlink), authorized pentests (cxg), the code graph and SAST, with the evidence kept in your repository. Needs the bravos CLI: npm install -g @bugb-technologies/bravos.';
10
+ const SHORT = 'Threat model, pentests, code graph and SAST for your coding agent.';
11
+ const AUTHOR = { name: 'BugB Technologies', url: 'https://www.bugb.io' };
12
+ const HOMEPAGE = 'https://docs.bugb.io/bravos/';
13
+ const REPOSITORY = `https://github.com/${RELEASE_REPO}`;
14
+ const LICENSE_ID = 'LicenseRef-Bravos-Software-Licence';
15
+ const KEYWORDS = ['security', 'threat-modeling', 'guardlink', 'pentest', 'cxg', 'sast', 'code-graph', 'skills'];
16
+ const SESSION_MATCHER = 'startup|resume|clear|compact';
17
+ const json = (v) => Buffer.from(JSON.stringify(v, null, 2) + '\n');
18
+ const text = (s) => Buffer.from(s.endsWith('\n') ? s : s + '\n');
19
+ function walk(dir) {
20
+ const out = [];
21
+ for (const e of readdirSync(dir, { withFileTypes: true })) {
22
+ const abs = join(dir, e.name);
23
+ if (e.isDirectory())
24
+ out.push(...walk(abs));
25
+ else if (e.isFile())
26
+ out.push(abs);
27
+ }
28
+ return out;
29
+ }
30
+ const guardlinkServer = () => ({ type: 'stdio', command: SERVERS.guardlink.command, args: [...SERVERS.guardlink.args] });
31
+ // @comment -- "Every file of the public plugin repository, computed and not written. Skills: Claude Code's copy uses ${CLAUDE_SKILL_DIR} and ${CLAUDE_PLUGIN_ROOT}, which it substitutes; every other copy uses the portable SKILL_DIR wording. In every copy a skill's `node .bravos/hooks/<hook>.mjs` is `bravos hook <hook>`, because a plugin install has no .bravos/hooks/. Session hooks are registered where the harness's plugin format and SessionStart output are known: Claude Code, Codex, Gemini CLI and Antigravity. Copilot, VS Code and Cursor get the skills and MCP, and the bravos skill loads the briefing itself."
32
+ export function releaseFiles(opts = {}) {
33
+ const v = version();
34
+ const files = [];
35
+ const add = (path, content) => files.push({ path, content });
36
+ const skills = listBundledSkills(opts.skillsDir);
37
+ const hooks = opts.hooksDir ?? hooksDir();
38
+ const hookFiles = walk(hooks).sort();
39
+ const licence = opts.licence ?? readFileSync(join(packageRoot(), 'LICENSE'));
40
+ const addSkills = (prefix, render) => {
41
+ for (const skill of skills) {
42
+ for (const f of skill.files) {
43
+ const md = f.rel.endsWith('.md');
44
+ add(`${prefix}skills/${skill.name}/${f.rel}`, md ? Buffer.from(render(f.content.toString('utf8'), f.rel)) : f.content);
45
+ }
46
+ }
47
+ };
48
+ const addHookScripts = (prefix) => {
49
+ for (const abs of hookFiles)
50
+ add(`${prefix}hooks/${relative(hooks, abs).split('\\').join('/')}`, readFileSync(abs));
51
+ };
52
+ const sessionGroup = (command) => ({ matcher: SESSION_MATCHER, hooks: [{ type: 'command', command, timeout: 30 }] });
53
+ const base = { name: NAME, version: v, description: DESCRIPTION, author: AUTHOR, homepage: HOMEPAGE, repository: REPOSITORY, license: LICENSE_ID, keywords: KEYWORDS };
54
+ // Claude Code: claude/, listed by .claude-plugin/marketplace.json.
55
+ addSkills('claude/', (c, rel) => viaCliHooks(renderPluginSkillFile(c, rel)));
56
+ addHookScripts('claude/');
57
+ add('claude/hooks/hooks.json', json({ hooks: { SessionStart: [sessionGroup('node "${CLAUDE_PLUGIN_ROOT}/hooks/session-start.mjs" --harness claude')] } }));
58
+ add('claude/.mcp.json', json({ mcpServers: { guardlink: SERVERS.guardlink } }));
59
+ add('claude/.claude-plugin/plugin.json', json(base));
60
+ add('claude/LICENSE', licence);
61
+ add('.claude-plugin/marketplace.json', json({
62
+ name: NAME,
63
+ owner: AUTHOR,
64
+ description: SHORT,
65
+ plugins: [{ name: NAME, source: './claude', description: DESCRIPTION, version: v, category: 'security', homepage: HOMEPAGE }],
66
+ }));
67
+ // Codex: codex/, listed by .agents/plugins/marketplace.json. Hooks at the plugin root, as Codex's own plugins keep them.
68
+ addSkills('codex/', renderPortableSkillFile);
69
+ addHookScripts('codex/');
70
+ add('codex/hooks.json', json({ hooks: { SessionStart: [sessionGroup('node "${PLUGIN_ROOT}/hooks/session-start.mjs" --harness codex')] } }));
71
+ add('codex/.mcp.json', json({ mcpServers: { guardlink: guardlinkServer() } }));
72
+ add('codex/.codex-plugin/plugin.json', json({
73
+ ...base,
74
+ skills: './skills/',
75
+ mcpServers: './.mcp.json',
76
+ interface: {
77
+ displayName: 'Bravos',
78
+ shortDescription: SHORT,
79
+ longDescription: DESCRIPTION,
80
+ developerName: AUTHOR.name,
81
+ category: 'Security',
82
+ capabilities: ['Read', 'Write'],
83
+ websiteURL: HOMEPAGE,
84
+ defaultPrompt: ['Where do I start on security here?', 'Threat-model this repository', 'Is this PR safe to open?'],
85
+ },
86
+ }));
87
+ add('codex/LICENSE', licence);
88
+ add('.agents/plugins/marketplace.json', json({
89
+ name: NAME,
90
+ interface: { displayName: 'Bravos' },
91
+ plugins: [{ name: NAME, source: { source: 'local', path: './codex' }, policy: { installation: 'AVAILABLE', authentication: 'ON_INSTALL' }, category: 'Security' }],
92
+ }));
93
+ // GitHub Copilot CLI and VS Code: copilot/, an Agent Plugins 1.0 plugin, listed by .github/plugin/marketplace.json.
94
+ addSkills('copilot/', renderPortableSkillFile);
95
+ add('copilot/mcp.json', json({ mcpServers: { guardlink: guardlinkServer() } }));
96
+ add('copilot/plugin.json', json({ $schema: 'https://agent-plugins.org/schemas/1.0.0/plugin.schema.json', ...base }));
97
+ add('copilot/LICENSE', licence);
98
+ add('.github/plugin/marketplace.json', json({
99
+ name: NAME,
100
+ owner: AUTHOR,
101
+ description: SHORT,
102
+ plugins: [{ name: NAME, source: './copilot', description: DESCRIPTION, version: v }],
103
+ }));
104
+ // Cursor (team marketplaces): cursor/, listed by .cursor-plugin/marketplace.json.
105
+ addSkills('cursor/', renderPortableSkillFile);
106
+ add('cursor/mcp.json', json({ mcpServers: { guardlink: guardlinkServer() } }));
107
+ add('cursor/.cursor-plugin/plugin.json', json(base));
108
+ add('cursor/LICENSE', licence);
109
+ add('.cursor-plugin/marketplace.json', json({
110
+ name: NAME,
111
+ owner: AUTHOR,
112
+ description: SHORT,
113
+ plugins: [{ name: NAME, source: './cursor', description: DESCRIPTION, version: v }],
114
+ }));
115
+ // The repository root: the Gemini CLI extension and the Antigravity plugin share skills/ and hooks/.
116
+ addSkills('', renderPortableSkillFile);
117
+ addHookScripts('');
118
+ add('gemini-extension.json', json({
119
+ name: NAME,
120
+ version: v,
121
+ description: DESCRIPTION,
122
+ contextFileName: 'GEMINI.md',
123
+ mcpServers: { guardlink: { command: SERVERS.guardlink.command, args: [...SERVERS.guardlink.args] } },
124
+ }));
125
+ add('GEMINI.md', text(GEMINI_CONTEXT));
126
+ add('hooks/hooks.json', json({ hooks: { SessionStart: [sessionGroup('node "${extensionPath}${/}hooks${/}session-start.mjs" --harness gemini')] } }));
127
+ // Antigravity runs a plugin's hooks with the plugin root as the working directory; SessionStart takes a flat list.
128
+ add('.antigravity-plugin/plugin.json', json({
129
+ ...base,
130
+ displayName: 'Bravos',
131
+ suggestedPrompts: ['Where do I start on security here?', 'Threat-model this repository', 'Is this PR safe to open?'],
132
+ }));
133
+ add('hooks.json', json({ bravos: { SessionStart: [{ type: 'command', command: 'node hooks/session-start.mjs --harness agy', timeout: 30 }] } }));
134
+ // Antigravity loads a plugin's context only from rules/; GEMINI.md is Gemini CLI's.
135
+ add('rules/bravos.md', text(GEMINI_CONTEXT));
136
+ add('mcp_config.json', json({ mcpServers: { guardlink: { command: SERVERS.guardlink.command, args: [...SERVERS.guardlink.args] } } }));
137
+ add('LICENSE', licence);
138
+ add('README.md', text(readme(v)));
139
+ return files.sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
140
+ }
141
+ const GEMINI_CONTEXT = `# Bravos
142
+
143
+ This extension adds the Bravos skills: the threat model as guardlink annotations,
144
+ authorized pentests with cxg, the code graph and SAST. Start with the \`bravos\`
145
+ skill. If no briefing is in context, run \`bravos hook session-start --briefing\`
146
+ once to print the repository's security picture.
147
+
148
+ The skills drive the \`bravos\` CLI (\`npm install -g @bugb-technologies/bravos\`),
149
+ \`guardlink\` and \`cxg\`. Never send a request to a host other than loopback
150
+ without a committed, reviewed \`.guardlink/scope.yaml\` and the user's own typed
151
+ authorization naming the target.`;
152
+ function readme(v) {
153
+ return `# Bravos plugins
154
+
155
+ Bravos ${v} for the coding agent you already use. Each plugin carries the same 14
156
+ skills and the guardlink MCP server; the session-start briefing loads by itself
157
+ where the agent supports it.
158
+
159
+ **First, install the CLI** (Node 20 or newer). The skills drive it:
160
+
161
+ \`\`\`bash
162
+ npm install -g @bugb-technologies/bravos
163
+ \`\`\`
164
+
165
+ Then install the plugin for your agent:
166
+
167
+ | Agent | Install |
168
+ | --- | --- |
169
+ | Claude Code | \`claude plugin marketplace add ${RELEASE_REPO}\` then \`claude plugin install bravos@bravos\` |
170
+ | Codex | \`codex plugin marketplace add ${RELEASE_REPO}\` then \`/plugins\` in Codex and install Bravos |
171
+ | GitHub Copilot CLI | \`copilot plugin marketplace add ${RELEASE_REPO}\` then \`copilot plugin install bravos@bravos\` |
172
+ | VS Code (agent plugins) | add \`${RELEASE_REPO}\` to \`chat.plugins.marketplaces\`, then install Bravos from the Agent Customizations view |
173
+ | Gemini CLI | \`gemini extensions install ${REPOSITORY}\` |
174
+ | Antigravity | \`agy plugin install ${REPOSITORY}\` |
175
+ | Cursor (Teams and Enterprise) | Dashboard → Plugins → Import from Repo → \`${REPOSITORY}\` |
176
+
177
+ You do not need a plugin: \`bravos init\` in a repository installs the same skills,
178
+ hooks and MCP wiring for every agent it finds, and commits them with the code.
179
+
180
+ ## Plans
181
+
182
+ The research loop is free for one researcher in one repository. The pre-PR gate
183
+ and autopilot, CI and nightly workflows, scheduled upkeep, workspaces, campaigns,
184
+ SAST and the code graph are on the Team plan: \`bravos account activate\` starts a
185
+ 30-day trial with no card. See [how licensing works](https://docs.bugb.io/bravos/concepts/how-licensing-works/).
186
+
187
+ ## Docs
188
+
189
+ [docs.bugb.io/bravos](${HOMEPAGE})
190
+
191
+ ## Licence
192
+
193
+ Bravos Software Licence: see [LICENSE](LICENSE). This repository is generated from
194
+ the Bravos release; changes go into Bravos itself, not here.
195
+ `;
196
+ }
197
+ // @comment -- "Writes the public plugin repository's tree into out: removes every tracked path the previous build wrote except .git/, then writes releaseFiles(). Returns the paths written."
198
+ export function writeRelease(out, opts = {}) {
199
+ const files = releaseFiles(opts);
200
+ mkdirSync(out, { recursive: true });
201
+ for (const e of readdirSync(out)) {
202
+ if (e === '.git')
203
+ continue;
204
+ rmSync(join(out, e), { recursive: true, force: true });
205
+ }
206
+ for (const f of files) {
207
+ const abs = join(out, f.path);
208
+ mkdirSync(dirname(abs), { recursive: true });
209
+ writeFileSync(abs, f.content);
210
+ }
211
+ return files.map((f) => f.path);
212
+ }
package/dist/skills.js CHANGED
@@ -68,6 +68,30 @@ export function renderPluginSkillFile(content, rel = 'SKILL.md') {
68
68
  .split('{{SKILLS_ROOT}}')
69
69
  .join('.claude/skills');
70
70
  }
71
+ // @comment -- "A plugin has no .bravos/hooks/ in the repository, so a skill's `node .bravos/hooks/<hook>.mjs` becomes `bravos hook <hook>`, which runs the same script from the installed CLI."
72
+ export function viaCliHooks(content) {
73
+ return content.replace(/node \.bravos\/hooks\/([\w-]+)\.mjs/g, 'bravos hook $1');
74
+ }
75
+ /** The line a portable SKILL.md carries after its frontmatter, saying what SKILL_DIR means. */
76
+ export const SKILL_DIR_NOTE = '> Paths that start with `SKILL_DIR/` are files in this skill\'s own folder, the folder that holds this SKILL.md.';
77
+ // @comment -- "Portable plugin render, for harnesses that substitute no variable inside SKILL.md (Codex, Copilot, VS Code, Cursor, Gemini, Antigravity): {{SKILL_DIR}} becomes the literal SKILL_DIR with a note saying it is this skill's folder; {{SKILLS_ROOT}}/bravos* becomes SKILL_DIR/../bravos*, a sibling in the same plugin; any other {{SKILLS_ROOT}} (guardlink's generated skills) stays at the repo's .claude/skills; hook scripts run through the CLI."
78
+ export function renderPortableSkillFile(content, rel = 'SKILL.md') {
79
+ const needsNote = rel === 'SKILL.md' && (content.includes('{{SKILL_DIR}}') || content.includes('{{SKILLS_ROOT}}/bravos'));
80
+ let out = viaCliHooks(withMarker(content, rel))
81
+ .split('{{SKILL_DIR}}')
82
+ .join('SKILL_DIR')
83
+ .split('{{SKILLS_ROOT}}/bravos')
84
+ .join('SKILL_DIR/../bravos')
85
+ .split('{{SKILLS_ROOT}}')
86
+ .join('.claude/skills');
87
+ if (needsNote) {
88
+ const lines = out.split('\n');
89
+ const at = lines.indexOf(MARKER);
90
+ lines.splice(at === -1 ? 0 : at + 1, 0, '', SKILL_DIR_NOTE);
91
+ out = lines.join('\n');
92
+ }
93
+ return out;
94
+ }
71
95
  // @comment -- "User-level render (bravos init --global): bravos's own paths become absolute under the home folder (skills in ~/.claude/skills, hook scripts in ~/.bravos/hooks), while {{SKILLS_ROOT}} for guardlink's generated skills stays at the repo's .claude/skills."
72
96
  export function renderGlobalSkillFile(home) {
73
97
  return (content, _harness, name, rel = 'SKILL.md') => withMarker(content, rel)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bugb-technologies/bravos",
3
- "version": "1.0.2",
3
+ "version": "1.0.3",
4
4
  "description": "Skills pack and installer that make Claude Code and Codex power users of guardlink (threat-model-as-code) and cert-x-gen (pentest-as-code).",
5
5
  "keywords": [
6
6
  "security",
@@ -37,7 +37,8 @@
37
37
  "test": "vitest run",
38
38
  "capture-help": "bash scripts/capture-help.sh",
39
39
  "prepublishOnly": "npm run build && npm test",
40
- "build:plugin": "npm run build && node scripts/build-plugin.mjs"
40
+ "build:plugin": "npm run build && node scripts/build-plugin.mjs",
41
+ "build:plugin-release": "node scripts/build-plugin-release.mjs"
41
42
  },
42
43
  "dependencies": {
43
44
  "commander": "^15.0.0",