sekura 0.0.0-stage → 0.1.0

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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Sekura Inc.
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 CHANGED
@@ -1,3 +1,63 @@
1
- # Temporary Holding Version
1
+ # sekura
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Connect your coding tool to [Sekura](https://sekura.ai) in one command.
4
+
5
+ ```bash
6
+ npx sekura@latest init
7
+ ```
8
+
9
+ Sekura is an AI red team as a service: it scans a GitHub repository you have granted it, exploits what it finds to prove each finding, and delivers a report. Its MCP server is remote, at `https://sekura.ai/mcp`, so this package runs no server and holds no credential. It writes that address into the coding tools on your machine, installs a skill that teaches the agent the quote-then-confirm rule, and gets out of the way.
10
+
11
+ ## What `init` does
12
+
13
+ 1. **Detects** the tools on this machine: Claude Code, Cursor, GitHub Copilot in VS Code, OpenAI Codex, Kiro, Cline, Zed, Antigravity, OpenCode, JetBrains Junie, Factory Droid, Tabnine, Devin and Goose.
14
+ 2. **Asks** which to connect, and whether for every project (your user configuration) or this project only.
15
+ 3. **Shows the plan** before anything is written: each file and the exact entry, or the tool's own command.
16
+ 4. **Applies** it, one step at a time, and says what each did. Files that are not plain JSON (Zed's `settings.json` with comments, for one) are never rewritten; you are shown the lines to add instead.
17
+ 5. **Checks** that the Sekura server answers, and tells you how to sign in from each tool.
18
+
19
+ Signing in happens in the tool: the first time it uses Sekura, a sekura.ai page opens in your browser. Sign in with GitHub and approve. Nothing to copy.
20
+
21
+ ```bash
22
+ npx sekura@latest init --yes # every detected tool, user scope, with the skill, no questions
23
+ npx sekura@latest init --dry-run # show the plan and change nothing
24
+ npx sekura@latest init --scope project # this project only: .mcp.json, .cursor/mcp.json, AGENTS.md …
25
+ npx sekura@latest init --tools claude,cursor --no-skill
26
+ npx sekura@latest status # what is connected
27
+ npx sekura@latest uninstall # remove the entries, the skill and the guidance block
28
+ ```
29
+
30
+ ## What it writes
31
+
32
+ | Tool | User scope | Project scope |
33
+ |---|---|---|
34
+ | Claude Code | `claude mcp add --transport http -s user sekura …`, or `~/.claude.json` → `mcpServers.sekura` | `.mcp.json` |
35
+ | Cursor | `~/.cursor/mcp.json` → `mcpServers.sekura` | `.cursor/mcp.json` |
36
+ | GitHub Copilot (VS Code) | the VS Code user `mcp.json` → `servers.sekura` | `.mcp.json` |
37
+ | OpenAI Codex | `codex mcp add sekura --url …`, or `~/.codex/config.toml` → `[mcp_servers.sekura]` | — |
38
+ | Kiro | `~/.kiro/settings/mcp.json` | `.kiro/settings/mcp.json` |
39
+ | Cline | `cline_mcp_settings.json` in VS Code's global storage | — |
40
+ | Zed | `~/.config/zed/settings.json` → `context_servers.sekura` (shown by hand when the file has comments) | — |
41
+ | Antigravity | `~/.gemini/config/mcp_config.json` → `serverUrl` | `.agents/mcp_config.json` |
42
+ | OpenCode | `~/.config/opencode/opencode.json` → `mcp.sekura` | `opencode.json` |
43
+ | JetBrains Junie | — | `.junie/mcp/mcp.json` |
44
+ | Factory Droid, Tabnine, Devin | the tool's own `mcp add` command | — |
45
+ | Goose | a link to the page | — |
46
+
47
+ The skill goes to `~/.agents/skills/sekura/SKILL.md` and, when Claude Code is present, `~/.claude/skills/sekura/SKILL.md`; in project scope to the same paths under the project, plus a section between `<!-- sekura:start -->` and `<!-- sekura:end -->` in `AGENTS.md` (and `CLAUDE.md` if one exists). Reruns replace only what sits between the markers.
48
+
49
+ ## What stays untouched
50
+
51
+ - Every other server, setting and file. The installer sets one key, `sekura`, and reads nothing else.
52
+ - Your source code. Nothing is read, uploaded or committed.
53
+ - Your credentials. There is no login step and no token on disk; the sign-in is the tool's own OAuth flow with sekura.ai.
54
+
55
+ Review before running: the source is this repository, the package is [`sekura` on npm](https://www.npmjs.com/package/sekura), and `--dry-run` prints every write the wizard would make.
56
+
57
+ ## Every tool, by hand
58
+
59
+ The page at [sekura.ai/connect](https://sekura.ai/connect/) has the file or screen for every tool, including the ones this installer cannot reach (claude.ai, Replit, Lovable, Bolt, v0, and the rest).
60
+
61
+ ## Licence
62
+
63
+ MIT.
@@ -0,0 +1,155 @@
1
+ // The tools `sekura init` can reach, and how each one takes a remote MCP
2
+ // server. One entry per tool, in the order of https://sekura.ai/connect/
3
+ // (docs/connector-master.md's order of reach). The config shapes are the
4
+ // ones the website's catalogue documents and tests against each vendor's
5
+ // docs (sekura-www, src/marketing/content/connect-tools.ts); a shape typed
6
+ // here that differs from the website's would be a connector that installs
7
+ // but never connects, so change both or neither.
8
+ //
9
+ // A tool is "present" when its config directory exists or its binary is on
10
+ // PATH. Presence decides what the wizard offers, never what it writes: the
11
+ // person chooses, and a tool they did not choose is not touched.
12
+ import { existsSync } from 'node:fs';
13
+ import { homedir, platform } from 'node:os';
14
+ import { join, delimiter } from 'node:path';
15
+ export const ENDPOINT = 'https://sekura.ai/mcp';
16
+ export const SERVER_NAME = 'sekura';
17
+ export const DOCS = 'https://sekura.ai/connect/';
18
+ /** The home directory, overridable for tests. */
19
+ export const home = () => process.env.SEKURA_HOME || homedir();
20
+ /** VS Code's user directory, where Copilot's `mcp.json` and Cline's settings live. */
21
+ export function codeUserDir(h = home()) {
22
+ switch (platform()) {
23
+ case 'darwin': return join(h, 'Library', 'Application Support', 'Code', 'User');
24
+ case 'win32': return join(process.env.APPDATA || join(h, 'AppData', 'Roaming'), 'Code', 'User');
25
+ default: return join(process.env.XDG_CONFIG_HOME || join(h, '.config'), 'Code', 'User');
26
+ }
27
+ }
28
+ const xdg = (h) => process.env.XDG_CONFIG_HOME || join(h, '.config');
29
+ /** True when `bin` is an executable on PATH. */
30
+ export function onPath(bin) {
31
+ const exts = platform() === 'win32' ? ['.exe', '.cmd', '.bat', ''] : [''];
32
+ for (const dir of (process.env.PATH || '').split(delimiter)) {
33
+ if (!dir)
34
+ continue;
35
+ for (const ext of exts)
36
+ if (existsSync(join(dir, bin + ext)))
37
+ return true;
38
+ }
39
+ return false;
40
+ }
41
+ const SIGN_IN = 'the first time the tool uses Sekura, a sekura.ai page opens in your browser: sign in with GitHub and approve.';
42
+ const url = { url: ENDPOINT };
43
+ const http = { type: 'http', url: ENDPOINT };
44
+ export const TOOLS = [
45
+ {
46
+ id: 'tools-claude', name: 'Claude Code',
47
+ detect: (h) => existsSync(join(h, '.claude')) || existsSync(join(h, '.claude.json')) || onPath('claude'),
48
+ user: (h) => [
49
+ { kind: 'command', bin: 'claude', add: ['mcp', 'add', '--transport', 'http', '-s', 'user', SERVER_NAME, ENDPOINT], remove: ['mcp', 'remove', '-s', 'user', SERVER_NAME] },
50
+ { kind: 'json', path: join(h, '.claude.json'), key: 'mcpServers', entry: http },
51
+ ],
52
+ project: (cwd) => [{ kind: 'json', path: join(cwd, '.mcp.json'), key: 'mcpServers', entry: http }],
53
+ signIn: `type /mcp in Claude Code, choose ${SERVER_NAME}, and sign in with GitHub.`,
54
+ },
55
+ {
56
+ id: 'tools-cursor', name: 'Cursor',
57
+ detect: (h) => existsSync(join(h, '.cursor')) || onPath('cursor'),
58
+ user: (h) => [{ kind: 'json', path: join(h, '.cursor', 'mcp.json'), key: 'mcpServers', entry: url }],
59
+ project: (cwd) => [{ kind: 'json', path: join(cwd, '.cursor', 'mcp.json'), key: 'mcpServers', entry: url }],
60
+ signIn: SIGN_IN,
61
+ },
62
+ {
63
+ id: 'tools-copilot', name: 'GitHub Copilot in VS Code',
64
+ detect: (h) => existsSync(codeUserDir(h)),
65
+ user: (h) => [{ kind: 'json', path: join(codeUserDir(h), 'mcp.json'), key: 'servers', entry: http }],
66
+ project: (cwd) => [{ kind: 'json', path: join(cwd, '.mcp.json'), key: 'mcpServers', entry: http }],
67
+ signIn: SIGN_IN,
68
+ },
69
+ {
70
+ id: 'tools-codex', name: 'OpenAI Codex',
71
+ detect: (h) => existsSync(join(h, '.codex')) || onPath('codex'),
72
+ user: (h) => [
73
+ { kind: 'command', bin: 'codex', add: ['mcp', 'add', SERVER_NAME, '--url', ENDPOINT], remove: ['mcp', 'remove', SERVER_NAME] },
74
+ { kind: 'toml', path: join(h, '.codex', 'config.toml'), table: `mcp_servers.${SERVER_NAME}`, lines: [`url = "${ENDPOINT}"`] },
75
+ ],
76
+ signIn: `run codex mcp login ${SERVER_NAME}, or sign in when Codex first uses Sekura.`,
77
+ },
78
+ {
79
+ id: 'tools-kiro', name: 'Kiro',
80
+ detect: (h) => existsSync(join(h, '.kiro')) || onPath('kiro'),
81
+ user: (h) => [{ kind: 'json', path: join(h, '.kiro', 'settings', 'mcp.json'), key: 'mcpServers', entry: url }],
82
+ project: (cwd) => [{ kind: 'json', path: join(cwd, '.kiro', 'settings', 'mcp.json'), key: 'mcpServers', entry: url }],
83
+ signIn: SIGN_IN,
84
+ },
85
+ {
86
+ id: 'tools-cline', name: 'Cline',
87
+ detect: (h) => existsSync(join(codeUserDir(h), 'globalStorage', 'saoudrizwan.claude-dev')),
88
+ user: (h) => [{ kind: 'json', path: join(codeUserDir(h), 'globalStorage', 'saoudrizwan.claude-dev', 'settings', 'cline_mcp_settings.json'), key: 'mcpServers', entry: { type: 'streamableHttp', url: ENDPOINT } }],
89
+ signIn: SIGN_IN,
90
+ },
91
+ {
92
+ id: 'tools-zed', name: 'Zed',
93
+ detect: (h) => existsSync(join(xdg(h), 'zed')) || onPath('zed'),
94
+ user: (h) => [{ kind: 'json', path: join(xdg(h), 'zed', 'settings.json'), key: 'context_servers', entry: url }],
95
+ signIn: `${SIGN_IN} Zed keeps the sign-in in your system keychain.`,
96
+ },
97
+ {
98
+ id: 'tools-antigravity', name: 'Antigravity',
99
+ detect: (h) => existsSync(join(h, '.gemini', 'config')),
100
+ user: (h) => [{ kind: 'json', path: join(h, '.gemini', 'config', 'mcp_config.json'), key: 'mcpServers', entry: { serverUrl: ENDPOINT } }],
101
+ project: (cwd) => [{ kind: 'json', path: join(cwd, '.agents', 'mcp_config.json'), key: 'mcpServers', entry: { serverUrl: ENDPOINT } }],
102
+ signIn: SIGN_IN,
103
+ },
104
+ {
105
+ id: 'tools-opencode', name: 'OpenCode',
106
+ detect: (h) => existsSync(join(xdg(h), 'opencode')) || onPath('opencode'),
107
+ user: (h) => [{ kind: 'json', path: join(xdg(h), 'opencode', 'opencode.json'), key: 'mcp', entry: { type: 'remote', url: ENDPOINT } }],
108
+ project: (cwd) => [{ kind: 'json', path: join(cwd, 'opencode.json'), key: 'mcp', entry: { type: 'remote', url: ENDPOINT } }],
109
+ signIn: `OpenCode registers itself with Sekura and opens the sign-in on first use; ${SIGN_IN}`,
110
+ },
111
+ {
112
+ id: 'tools-jetbrains', name: 'JetBrains Junie',
113
+ detect: (h, cwd) => existsSync(join(cwd, '.junie')) || existsSync(join(h, '.junie')),
114
+ user: () => [{ kind: 'link', href: `${DOCS}#tools-jetbrains`, note: 'Junie reads a project file; run sekura init --scope project in the project, or follow the page.' }],
115
+ project: (cwd) => [{ kind: 'json', path: join(cwd, '.junie', 'mcp', 'mcp.json'), key: 'mcpServers', entry: url }],
116
+ signIn: `type /mcp in Junie and sign in with GitHub.`,
117
+ },
118
+ {
119
+ id: 'tools-factory', name: 'Factory Droid',
120
+ detect: (h) => existsSync(join(h, '.factory')) || onPath('droid'),
121
+ user: () => [
122
+ { kind: 'command', bin: 'droid', add: ['mcp', 'add', SERVER_NAME, ENDPOINT, '--type', 'http'], remove: ['mcp', 'remove', SERVER_NAME] },
123
+ { kind: 'link', href: `${DOCS}#tools-factory`, note: 'droid is not on PATH; run the command from the page in a terminal where it is.' },
124
+ ],
125
+ signIn: `type /mcp in Droid and sign in to ${SERVER_NAME}.`,
126
+ },
127
+ {
128
+ id: 'tools-tabnine', name: 'Tabnine CLI',
129
+ detect: () => onPath('tabnine'),
130
+ user: () => [{ kind: 'command', bin: 'tabnine', add: ['mcp', 'add', SERVER_NAME, ENDPOINT, '--type', 'http'], remove: ['mcp', 'remove', SERVER_NAME] }],
131
+ signIn: 'run /mcp auth in the Tabnine CLI.',
132
+ },
133
+ {
134
+ id: 'tools-devin', name: 'Devin Desktop and CLI',
135
+ detect: () => onPath('devin'),
136
+ user: () => [{ kind: 'command', bin: 'devin', add: ['mcp', 'add', SERVER_NAME, ENDPOINT], remove: ['mcp', 'remove', SERVER_NAME] }],
137
+ signIn: `run devin mcp login ${SERVER_NAME}.`,
138
+ },
139
+ {
140
+ id: 'tools-goose', name: 'Goose',
141
+ detect: (h) => existsSync(join(xdg(h), 'goose')) || onPath('goose'),
142
+ user: () => [{ kind: 'link', href: `${DOCS}#tools-goose`, note: 'Goose takes a goose://extension link or goose configure; the page has both.' }],
143
+ signIn: SIGN_IN,
144
+ },
145
+ ];
146
+ export const toolById = (id) => TOOLS.find((t) => t.id === id);
147
+ /** The first target that can be applied: a command only when its binary is on PATH. */
148
+ export function pickTarget(targets) {
149
+ for (const t of targets) {
150
+ if (t.kind === 'command' && !onPath(t.bin))
151
+ continue;
152
+ return t;
153
+ }
154
+ return null;
155
+ }
package/dist/cli.js ADDED
@@ -0,0 +1,35 @@
1
+ #!/usr/bin/env node
2
+ // sekura: connect your coding tool to Sekura in one command.
3
+ //
4
+ // npx sekura@latest init
5
+ //
6
+ // Sekura's MCP server is remote (https://sekura.ai/mcp), so this package
7
+ // runs no server and holds no credential: it writes the server's address
8
+ // into the tools on this machine, installs the skill that teaches an agent
9
+ // the quote-then-confirm rule, and gets out of the way. Signing in happens
10
+ // in the tool, on a sekura.ai page, the first time it uses Sekura.
11
+ import { Command } from 'commander';
12
+ import { createRequire } from 'node:module';
13
+ import { initCommand } from './commands/init.js';
14
+ import { statusCommand } from './commands/status.js';
15
+ import { uninstallCommand } from './commands/uninstall.js';
16
+ import { DOCS } from './catalogue.js';
17
+ const { version } = createRequire(import.meta.url)('../package.json');
18
+ const program = new Command();
19
+ program
20
+ .name('sekura')
21
+ .description(`Connect your coding tool to Sekura.\n\n npx sekura@latest init\n\nDocumentation: ${DOCS}`)
22
+ .version(version)
23
+ .addCommand(initCommand, { isDefault: false })
24
+ .addCommand(statusCommand)
25
+ .addCommand(uninstallCommand);
26
+ try {
27
+ await program.parseAsync(process.argv);
28
+ }
29
+ catch (e) {
30
+ // Ctrl-C inside a prompt is an exit, not an error.
31
+ if (e?.name === 'ExitPromptError')
32
+ process.exit(130);
33
+ process.stderr.write(`\n ${e.message}\n\n`);
34
+ process.exit(1);
35
+ }
@@ -0,0 +1,126 @@
1
+ // `sekura init`: detect, choose, review, apply, verify. Under a minute, and
2
+ // nothing is written before the review is shown and (unless --yes) agreed.
3
+ import { Command } from 'commander';
4
+ import { checkbox, confirm, select } from '@inquirer/prompts';
5
+ import { DOCS, ENDPOINT, TOOLS } from '../catalogue.js';
6
+ import { applyStep, buildPlan, describeStep, detectTools } from '../plan.js';
7
+ import { verifyEndpoint } from '../verify.js';
8
+ import { banner, cross, dot, say, tick } from '../ui.js';
9
+ export async function runInit(opts) {
10
+ const interactive = !opts.yes && process.stdin.isTTY && process.stderr.isTTY;
11
+ const cwd = opts.cwd ?? process.cwd();
12
+ banner();
13
+ // 1. Detect.
14
+ const detected = detectTools(cwd);
15
+ let chosen;
16
+ if (opts.tools) {
17
+ chosen = opts.tools.split(',').map((s) => s.trim()).filter(Boolean).map((s) => (s.startsWith('tools-') ? s : `tools-${s}`));
18
+ }
19
+ else if (detected.length === 0) {
20
+ say(' No supported coding tool was found on this machine.');
21
+ say(` Sekura connects to Claude Code, Cursor, GitHub Copilot, OpenAI Codex and nearly forty other tools; the page has every one: ${DOCS}`);
22
+ return { written: 0, byHand: 0, failed: 0 };
23
+ }
24
+ else if (!interactive) {
25
+ chosen = detected.map((t) => t.id);
26
+ }
27
+ else {
28
+ say(` Found ${detected.length === 1 ? 'one tool' : `${detected.length} tools`}: ${detected.map((t) => t.name).join(', ')}`);
29
+ say();
30
+ chosen = await checkbox({
31
+ message: 'Connect Sekura to',
32
+ choices: detected.map((t) => ({ name: t.name, value: t.id, checked: true })),
33
+ required: true,
34
+ });
35
+ }
36
+ // 2. Scope and skill.
37
+ let scope = opts.scope ?? 'user';
38
+ if (!opts.scope && interactive) {
39
+ scope = await select({
40
+ message: 'Where',
41
+ choices: [
42
+ { name: 'For me, in every project (your user configuration)', value: 'user' },
43
+ { name: `This project only (${cwd})`, value: 'project' },
44
+ ],
45
+ });
46
+ }
47
+ let skill = opts.skill !== false;
48
+ if (opts.skill === undefined && interactive) {
49
+ skill = await confirm({ message: 'Also install the sekura skill, which teaches the agent the quote-then-confirm rule?', default: true });
50
+ }
51
+ // 3. The plan, shown before anything happens.
52
+ const plan = buildPlan({ tools: chosen, scope, skill, cwd });
53
+ say();
54
+ say(opts.dryRun ? ' Dry run. This would:' : ' This will:');
55
+ for (const s of plan.steps)
56
+ say(`${dot} ${describeStep(s, !!opts.dryRun)}`);
57
+ for (const s of plan.skipped)
58
+ say(`${dot} ${s.tool.name}: skipped, ${s.why}`);
59
+ say(`${dot} touch nothing else: other servers, settings and files stay as they are`);
60
+ say();
61
+ if (opts.dryRun)
62
+ return { written: 0, byHand: 0, failed: 0 };
63
+ if (interactive && !(await confirm({ message: 'Apply?', default: true }))) {
64
+ say(' Nothing was changed.');
65
+ return { written: 0, byHand: 0, failed: 0 };
66
+ }
67
+ // 4. Apply, one step at a time, saying what each did.
68
+ let written = 0, byHand = 0, failed = 0;
69
+ const signIns = new Map();
70
+ for (const s of plan.steps) {
71
+ const r = applyStep(s);
72
+ switch (r.status) {
73
+ case 'written':
74
+ case 'ran':
75
+ written++;
76
+ say(`${tick} ${r.where}`);
77
+ break;
78
+ case 'removed':
79
+ case 'absent': break;
80
+ case 'unchanged':
81
+ say(`${tick} ${r.where} (already there)`);
82
+ break;
83
+ case 'by-hand':
84
+ byHand++;
85
+ say(`${cross} ${r.where}: ${r.why}`);
86
+ if (r.snippet)
87
+ say(r.snippet.split('\n').map((l) => ' ' + l).join('\n'));
88
+ break;
89
+ case 'failed':
90
+ failed++;
91
+ say(`${cross} ${r.where}: ${r.why}`);
92
+ break;
93
+ }
94
+ if (s.kind === 'tool' && (r.status === 'written' || r.status === 'ran' || r.status === 'unchanged'))
95
+ signIns.set(s.tool.name, s.tool.signIn);
96
+ }
97
+ // 5. Verify the address, and say what comes next.
98
+ if (opts.verify !== false) {
99
+ const v = await verifyEndpoint(ENDPOINT);
100
+ say(`${v.ok ? tick : cross} ${v.detail}`);
101
+ }
102
+ say();
103
+ if (signIns.size) {
104
+ say(' Next, sign in once per tool:');
105
+ for (const [name, how] of signIns)
106
+ say(`${dot} ${name}: ${how}`);
107
+ say();
108
+ say(' Then ask it which of your repositories Sekura can scan, and for a quote to scan one.');
109
+ }
110
+ say(` Documentation and every other tool: ${DOCS}`);
111
+ say();
112
+ return { written, byHand, failed };
113
+ }
114
+ export const initCommand = new Command('init')
115
+ .description('Connect the coding tools on this machine to Sekura, and install the sekura skill.')
116
+ .option('-y, --yes', 'apply without asking; every detected tool, user scope, with the skill')
117
+ .option('--no-skill', 'the MCP server only: no skill, no guidance block')
118
+ .option('--scope <scope>', 'user (every project) or project (this directory)', (v) => { if (v !== 'user' && v !== 'project')
119
+ throw new Error('--scope is user or project'); return v; })
120
+ .option('--tools <ids>', `only these, comma-separated: ${TOOLS.map((t) => t.id.replace('tools-', '')).join(', ')}`)
121
+ .option('--dry-run', 'show the plan and change nothing')
122
+ .option('--no-verify', 'do not check that the Sekura server answers')
123
+ .action(async (opts) => {
124
+ const r = await runInit(opts);
125
+ process.exitCode = r.failed ? 1 : 0;
126
+ });
@@ -0,0 +1,36 @@
1
+ // `sekura status`: which tools are here, which carry Sekura, and whether the
2
+ // server answers. Reads only.
3
+ import { Command } from 'commander';
4
+ import { ENDPOINT } from '../catalogue.js';
5
+ import { buildPlan, detectTools, stepPresent } from '../plan.js';
6
+ import { verifyEndpoint } from '../verify.js';
7
+ import { cross, dot, say, tick } from '../ui.js';
8
+ export async function runStatus(opts) {
9
+ const cwd = opts.cwd ?? process.cwd();
10
+ const scope = opts.scope ?? 'user';
11
+ const detected = detectTools(cwd);
12
+ say();
13
+ if (!detected.length)
14
+ say(' No supported coding tool found on this machine.');
15
+ const plan = buildPlan({ tools: detected.map((t) => t.id), scope, skill: true, cwd });
16
+ for (const s of plan.steps) {
17
+ const p = stepPresent(s);
18
+ const mark = p === true ? tick : p === 'unknown' ? dot : cross;
19
+ const noun = s.kind === 'tool' ? 'connected' : 'installed';
20
+ const word = p === true ? noun : p === 'unknown' ? (s.kind === 'tool' && s.target.kind === 'command' ? 'kept by the tool itself; ask it' : 'its file has comments; look by hand') : `not ${noun}`;
21
+ const label = s.kind === 'tool' ? s.tool.name : s.kind === 'skill' ? `skill ${s.path}` : `guidance ${s.path}`;
22
+ say(`${mark} ${label}: ${word}`);
23
+ }
24
+ for (const s of plan.skipped)
25
+ say(`${dot} ${s.tool.name}: ${s.why}`);
26
+ if (opts.verify !== false) {
27
+ const v = await verifyEndpoint(ENDPOINT);
28
+ say(`${v.ok ? tick : cross} ${v.detail}`);
29
+ }
30
+ say();
31
+ }
32
+ export const statusCommand = new Command('status')
33
+ .description('Show which tools on this machine are connected to Sekura.')
34
+ .option('--scope <scope>', 'user or project', 'user')
35
+ .option('--no-verify', 'do not check that the Sekura server answers')
36
+ .action(async (opts) => { await runStatus(opts); });
@@ -0,0 +1,53 @@
1
+ // `sekura uninstall`: take the sekura entry out of every tool it was written
2
+ // to, and the skill and guidance with it unless asked to keep them. Nothing
3
+ // else in any file is touched. Sign-ins the tool keeps in its own store (a
4
+ // keychain entry, a token) are the tool's to forget; the connection can also
5
+ // be revoked on sekura.ai under Settings → Agents and API keys.
6
+ import { Command } from 'commander';
7
+ import { confirm } from '@inquirer/prompts';
8
+ import { buildPlan, describeStep, detectTools, removeStep } from '../plan.js';
9
+ import { cross, dot, say, tick } from '../ui.js';
10
+ export async function runUninstall(opts) {
11
+ const cwd = opts.cwd ?? process.cwd();
12
+ const scope = opts.scope ?? 'user';
13
+ const interactive = !opts.yes && process.stdin.isTTY && process.stderr.isTTY;
14
+ const plan = buildPlan({ tools: detectTools(cwd).map((t) => t.id), scope, skill: !opts.keepSkill, cwd });
15
+ say();
16
+ say(' This will remove Sekura from:');
17
+ for (const s of plan.steps)
18
+ say(`${dot} ${describeStep(s).split(':')[0]}`);
19
+ say(' and change nothing else.');
20
+ say();
21
+ if (interactive && !(await confirm({ message: 'Remove?', default: false }))) {
22
+ say(' Nothing was changed.');
23
+ return 0;
24
+ }
25
+ let failed = 0;
26
+ for (const s of plan.steps) {
27
+ const r = removeStep(s);
28
+ if (r.status === 'removed' || r.status === 'ran')
29
+ say(`${tick} ${r.where}`);
30
+ else if (r.status === 'absent')
31
+ say(`${dot} ${r.where}: nothing to remove`);
32
+ // A file this installer never rewrites (comments) is one it never wrote
33
+ // to either; the person is told where to look, and nothing has failed.
34
+ else if (r.status === 'by-hand')
35
+ say(`${dot} ${r.where}: ${r.why}; ${r.snippet} by hand if it is there`);
36
+ else {
37
+ failed++;
38
+ say(`${cross} ${r.where}: ${'why' in r ? r.why : r.status}`);
39
+ }
40
+ }
41
+ say();
42
+ say(' To end the connection on Sekura\'s side too: https://sekura.ai/app/settings → Agents and API keys.');
43
+ say();
44
+ return failed;
45
+ }
46
+ export const uninstallCommand = new Command('uninstall')
47
+ .description('Remove Sekura from every tool on this machine, and the skill.')
48
+ .option('-y, --yes', 'remove without asking')
49
+ .option('--keep-skill', 'leave the skill and the guidance block in place')
50
+ .option('--scope <scope>', 'user or project', 'user')
51
+ .action(async (opts) => {
52
+ process.exitCode = (await runUninstall(opts)) ? 1 : 0;
53
+ });
package/dist/plan.js ADDED
@@ -0,0 +1,72 @@
1
+ // The plan: what `init` would do on this machine for the tools the person
2
+ // chose, built once and shown before anything is written, then applied
3
+ // step by step. `status` and `uninstall` read the same plan, so the three
4
+ // commands cannot disagree about where Sekura lives.
5
+ import { existsSync } from 'node:fs';
6
+ import { TOOLS, home, onPath, pickTarget, toolById } from './catalogue.js';
7
+ import { apply, describe, has, remove } from './writers.js';
8
+ import { guidanceFiles, hasGuidance, removeGuidance, removeSkill, skillPaths, writeGuidance, writeSkill } from './skill.js';
9
+ export function detectTools(cwd = process.cwd()) {
10
+ const h = home();
11
+ return TOOLS.filter((t) => t.detect(h, cwd));
12
+ }
13
+ export function buildPlan(opts) {
14
+ const cwd = opts.cwd ?? process.cwd();
15
+ const root = opts.scope === 'user' ? home() : cwd;
16
+ const steps = [];
17
+ const skipped = [];
18
+ for (const id of opts.tools) {
19
+ const tool = toolById(id);
20
+ if (!tool)
21
+ throw new Error(`no tool named ${id}; one of ${TOOLS.map((t) => t.id).join(', ')}`);
22
+ const targets = opts.scope === 'user' ? tool.user(home()) : tool.project?.(cwd);
23
+ if (!targets) {
24
+ skipped.push({ tool, why: 'has no project-level configuration; run with --scope user' });
25
+ continue;
26
+ }
27
+ const target = pickTarget(targets);
28
+ if (!target) {
29
+ skipped.push({ tool, why: 'its command is not on PATH' });
30
+ continue;
31
+ }
32
+ steps.push({ kind: 'tool', tool, target });
33
+ }
34
+ if (opts.skill) {
35
+ const claude = opts.tools.includes('tools-claude') || existsSync(`${home()}/.claude`) || onPath('claude');
36
+ for (const p of skillPaths(root, claude, opts.scope))
37
+ steps.push({ kind: 'skill', path: p });
38
+ if (opts.scope === 'project')
39
+ for (const p of guidanceFiles(cwd))
40
+ steps.push({ kind: 'guidance', path: p });
41
+ }
42
+ return { scope: opts.scope, root, steps, skipped };
43
+ }
44
+ export function describeStep(s, dryRun = false) {
45
+ const verb = dryRun ? 'would ' : '';
46
+ switch (s.kind) {
47
+ case 'tool': return `${s.tool.name}: ${describe(s.target, dryRun)}`;
48
+ case 'skill': return `skill: ${verb}write ${s.path}`;
49
+ case 'guidance': return `guidance: ${verb}add a marked Sekura section to ${s.path}`;
50
+ }
51
+ }
52
+ export function applyStep(s) {
53
+ switch (s.kind) {
54
+ case 'tool': return apply(s.target);
55
+ case 'skill': return writeSkill(s.path);
56
+ case 'guidance': return writeGuidance(s.path);
57
+ }
58
+ }
59
+ export function removeStep(s) {
60
+ switch (s.kind) {
61
+ case 'tool': return remove(s.target);
62
+ case 'skill': return removeSkill(s.path);
63
+ case 'guidance': return removeGuidance(s.path);
64
+ }
65
+ }
66
+ export function stepPresent(s) {
67
+ switch (s.kind) {
68
+ case 'tool': return has(s.target);
69
+ case 'skill': return existsSync(s.path);
70
+ case 'guidance': return hasGuidance(s.path);
71
+ }
72
+ }
package/dist/skill.js ADDED
@@ -0,0 +1,91 @@
1
+ // The skill and the guidance block: what teaches an agent the rule that
2
+ // every Sekura scan is quoted first and started only after the person agrees.
3
+ //
4
+ // The skill is the one in sekuraai/sekura-plugins (the Claude Code plugin's
5
+ // skills/sekura/SKILL.md), shipped inside this package so an install that
6
+ // cannot reach GitHub still gets it. Written under ~/.agents/skills/ (the
7
+ // Agent Skills location several tools read) and, where Claude Code is
8
+ // present, under ~/.claude/skills/. In project scope the same two paths
9
+ // under the project, plus a marked block in AGENTS.md (and CLAUDE.md when
10
+ // one exists): reruns replace only what sits between the markers.
11
+ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
12
+ import { dirname, join } from 'node:path';
13
+ import { fileURLToPath } from 'node:url';
14
+ import { DOCS, ENDPOINT, SERVER_NAME } from './catalogue.js';
15
+ const here = dirname(fileURLToPath(import.meta.url));
16
+ /** The packaged skill: dist/ sits beside skills/ in the published package. */
17
+ export const SKILL_SOURCE = join(here, '..', 'skills', SERVER_NAME, 'SKILL.md');
18
+ /** LF whatever the checkout did: a Windows clone with autocrlf would otherwise
19
+ * install a skill whose every line ends in \r, and a rerun would rewrite it. */
20
+ export const skillText = () => readFileSync(SKILL_SOURCE, 'utf8').replace(/\r\n/g, '\n');
21
+ export function skillPaths(root, claude, scope) {
22
+ const out = [join(root, '.agents', 'skills', SERVER_NAME, 'SKILL.md')];
23
+ if (claude || (scope === 'project' && existsSync(join(root, '.claude'))))
24
+ out.push(join(root, '.claude', 'skills', SERVER_NAME, 'SKILL.md'));
25
+ return out;
26
+ }
27
+ export function writeSkill(path) {
28
+ const text = skillText();
29
+ if (existsSync(path) && readFileSync(path, 'utf8') === text)
30
+ return { status: 'unchanged', where: path };
31
+ mkdirSync(dirname(path), { recursive: true });
32
+ writeFileSync(path, text);
33
+ return { status: 'written', where: path };
34
+ }
35
+ export function removeSkill(path) {
36
+ if (!existsSync(path))
37
+ return { status: 'absent', where: path };
38
+ rmSync(dirname(path), { recursive: true, force: true });
39
+ return { status: 'removed', where: path };
40
+ }
41
+ // ── the guidance block ──────────────────────────────────────────────────────
42
+ const START = '<!-- sekura:start -->';
43
+ const END = '<!-- sekura:end -->';
44
+ export const GUIDANCE = `${START}
45
+ ## Sekura
46
+
47
+ Sekura (the \`${SERVER_NAME}\` MCP server, ${ENDPOINT}) scans a GitHub repository for
48
+ vulnerabilities and proves each finding. Every scan or plan is quoted first: show
49
+ the user the preview as it stands, with the price and the authorization sentence,
50
+ and call the tool again only after they have agreed in the conversation. The
51
+ \`${SERVER_NAME}\` skill (.agents/skills/${SERVER_NAME}/SKILL.md) has the tool names and the rules.
52
+ Documentation: ${DOCS}
53
+ ${END}
54
+ `;
55
+ export function guidanceFiles(cwd) {
56
+ const out = [join(cwd, 'AGENTS.md')];
57
+ if (existsSync(join(cwd, 'CLAUDE.md')))
58
+ out.push(join(cwd, 'CLAUDE.md'));
59
+ return out;
60
+ }
61
+ export function writeGuidance(path) {
62
+ const text = existsSync(path) ? readFileSync(path, 'utf8') : '';
63
+ const a = text.indexOf(START), b = text.indexOf(END);
64
+ if (a >= 0 && b > a) {
65
+ const current = text.slice(a, b + END.length + 1);
66
+ if (current === GUIDANCE)
67
+ return { status: 'unchanged', where: path };
68
+ writeFileSync(path, text.slice(0, a) + GUIDANCE + text.slice(b + END.length).replace(/^\n/, ''));
69
+ return { status: 'written', where: path };
70
+ }
71
+ const sep = !text ? '' : text.endsWith('\n\n') ? '' : text.endsWith('\n') ? '\n' : '\n\n';
72
+ writeFileSync(path, text + sep + GUIDANCE);
73
+ return { status: 'written', where: path };
74
+ }
75
+ export function removeGuidance(path) {
76
+ if (!existsSync(path))
77
+ return { status: 'absent', where: path };
78
+ const text = readFileSync(path, 'utf8');
79
+ const a = text.indexOf(START), b = text.indexOf(END);
80
+ if (a < 0 || b < a)
81
+ return { status: 'absent', where: path };
82
+ const out = (text.slice(0, a) + text.slice(b + END.length).replace(/^\n/, '')).replace(/\n{3,}/g, '\n\n').replace(/\n+$/, '\n');
83
+ if (!out.trim())
84
+ rmSync(path);
85
+ else
86
+ writeFileSync(path, out);
87
+ return { status: 'removed', where: path };
88
+ }
89
+ export function hasGuidance(path) {
90
+ return existsSync(path) && readFileSync(path, 'utf8').includes(GUIDANCE);
91
+ }
package/dist/ui.js ADDED
@@ -0,0 +1,13 @@
1
+ // Output goes to stderr with plain words and no colour library, so it reads
2
+ // the same in every terminal and in a log. stdout stays empty: the person
3
+ // may pipe it.
4
+ export const say = (s = '') => { process.stderr.write(s + '\n'); };
5
+ export const tick = ' ✓';
6
+ export const cross = ' ✗';
7
+ export const dot = ' ·';
8
+ export function banner() {
9
+ say();
10
+ say(' Sekura — connect your coding tool');
11
+ say(' Every scan is quoted first and started only after you agree.');
12
+ say();
13
+ }
package/dist/verify.js ADDED
@@ -0,0 +1,25 @@
1
+ // Is the Sekura MCP server answering? A keyless GET to the endpoint is a 401
2
+ // in JSON (the server asks for a sign-in), and the protected-resource
3
+ // document beside it is a 200. Anything else means the address this
4
+ // installer wrote will not connect, which the person should hear now rather
5
+ // than from their tool later.
6
+ import { ENDPOINT } from './catalogue.js';
7
+ export async function verifyEndpoint(endpoint = ENDPOINT, timeoutMs = 8000) {
8
+ const ctl = new AbortController();
9
+ const timer = setTimeout(() => ctl.abort(), timeoutMs);
10
+ try {
11
+ const r = await fetch(endpoint, { method: 'GET', headers: { accept: 'application/json' }, signal: ctl.signal, redirect: 'manual' });
12
+ const type = r.headers.get('content-type') || '';
13
+ if (r.status === 401 && type.includes('json'))
14
+ return { ok: true, detail: `${endpoint} answers 401 (sign-in required), as it should before you sign in` };
15
+ if (r.status === 200)
16
+ return { ok: true, detail: `${endpoint} answers 200` };
17
+ return { ok: false, detail: `${endpoint} answered ${r.status} ${type || ''}`.trim() };
18
+ }
19
+ catch (e) {
20
+ return { ok: false, detail: `${endpoint} could not be reached: ${e.message}` };
21
+ }
22
+ finally {
23
+ clearTimeout(timer);
24
+ }
25
+ }
@@ -0,0 +1,164 @@
1
+ // Writing one `sekura` entry into a tool's configuration, and taking it out
2
+ // again, without touching anything else in the file.
3
+ //
4
+ // A JSON file is parsed, the entry set under its key, and the file written
5
+ // back with the indent it had. A file that does not parse is never rewritten:
6
+ // Zed's settings.json and OpenCode's opencode.jsonc may carry comments, and a
7
+ // rewrite that dropped them would cost the person their own configuration to
8
+ // save them a copy-paste. Such a file is reported with the lines to add by
9
+ // hand. A TOML file (Codex) is edited as text: the table is appended when
10
+ // absent, replaced when present, and the rest of the file is left byte for
11
+ // byte.
12
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
13
+ import { dirname } from 'node:path';
14
+ import { spawnSync } from 'node:child_process';
15
+ import { SERVER_NAME } from './catalogue.js';
16
+ const describeJson = (t) => JSON.stringify({ [t.key]: { [SERVER_NAME]: t.entry } }, null, 2);
17
+ /** What applying the target would do, for the review before any write. */
18
+ export function describe(t, dryRun = false) {
19
+ const verb = dryRun ? 'would ' : '';
20
+ switch (t.kind) {
21
+ case 'json': return `${verb}write ${t.path}: ${t.key}.${SERVER_NAME} = ${JSON.stringify(t.entry)}`;
22
+ case 'toml': return `${verb}write ${t.path}: [${t.table}] ${t.lines.join('; ')}`;
23
+ case 'command': return `${verb}run ${t.bin} ${t.add.join(' ')}`;
24
+ case 'link': return `by hand: ${t.note} ${t.href}`;
25
+ }
26
+ }
27
+ function detectIndent(text) {
28
+ const m = /\n([ \t]+)"/.exec(text);
29
+ return m ? m[1] : 2;
30
+ }
31
+ function readJson(path) {
32
+ if (!existsSync(path))
33
+ return { data: {}, indent: 2, trailingNewline: true };
34
+ const text = readFileSync(path, 'utf8');
35
+ if (!text.trim())
36
+ return { data: {}, indent: 2, trailingNewline: true };
37
+ try {
38
+ const data = JSON.parse(text);
39
+ if (!data || typeof data !== 'object' || Array.isArray(data))
40
+ return { invalid: 'its top level is not an object' };
41
+ return { data, indent: detectIndent(text), trailingNewline: text.endsWith('\n') };
42
+ }
43
+ catch {
44
+ return { invalid: 'it is not plain JSON (comments or trailing commas, which this tool allows and this installer will not rewrite)' };
45
+ }
46
+ }
47
+ export function applyJson(t) {
48
+ const r = readJson(t.path);
49
+ if ('invalid' in r)
50
+ return { status: 'by-hand', where: t.path, why: r.invalid, snippet: describeJson(t) };
51
+ const servers = (r.data[t.key] && typeof r.data[t.key] === 'object' ? r.data[t.key] : {});
52
+ const current = servers[SERVER_NAME];
53
+ if (current && JSON.stringify(current) === JSON.stringify(t.entry))
54
+ return { status: 'unchanged', where: t.path };
55
+ servers[SERVER_NAME] = t.entry;
56
+ r.data[t.key] = servers;
57
+ mkdirSync(dirname(t.path), { recursive: true });
58
+ writeFileSync(t.path, JSON.stringify(r.data, null, r.indent) + (r.trailingNewline ? '\n' : ''));
59
+ return { status: 'written', where: t.path };
60
+ }
61
+ export function removeJson(t) {
62
+ const r = readJson(t.path);
63
+ if ('invalid' in r)
64
+ return { status: 'by-hand', where: t.path, why: r.invalid, snippet: `remove ${t.key}.${SERVER_NAME}` };
65
+ const servers = r.data[t.key];
66
+ if (!servers || !(SERVER_NAME in servers))
67
+ return { status: 'absent', where: t.path };
68
+ delete servers[SERVER_NAME];
69
+ writeFileSync(t.path, JSON.stringify(r.data, null, r.indent) + (r.trailingNewline ? '\n' : ''));
70
+ return { status: 'removed', where: t.path };
71
+ }
72
+ /** Does this JSON file carry our entry as it should be? */
73
+ export function hasJson(t) {
74
+ const r = readJson(t.path);
75
+ if ('invalid' in r)
76
+ return 'unreadable';
77
+ const servers = r.data[t.key];
78
+ return !!servers && JSON.stringify(servers[SERVER_NAME]) === JSON.stringify(t.entry);
79
+ }
80
+ // ── TOML, as text ───────────────────────────────────────────────────────────
81
+ const tomlBlock = (t) => `[${t.table}]\n${t.lines.join('\n')}\n`;
82
+ /** The span of `[table]` up to the next header or the end, or null. */
83
+ function tomlSpan(text, table) {
84
+ const header = new RegExp(`^\\[${table.replace(/\./g, '\\.')}\\]\\s*$`, 'm');
85
+ const m = header.exec(text);
86
+ if (!m)
87
+ return null;
88
+ const rest = text.slice(m.index + m[0].length);
89
+ const next = /^\s*\[/m.exec(rest);
90
+ return [m.index, next ? m.index + m[0].length + next.index : text.length];
91
+ }
92
+ export function applyToml(t) {
93
+ const text = existsSync(t.path) ? readFileSync(t.path, 'utf8') : '';
94
+ const block = tomlBlock(t);
95
+ const span = tomlSpan(text, t.table);
96
+ if (span) {
97
+ const current = text.slice(span[0], span[1]).trim();
98
+ if (current === block.trim())
99
+ return { status: 'unchanged', where: t.path };
100
+ const out = text.slice(0, span[0]) + block + (text.slice(span[1]).startsWith('\n') ? '' : '\n') + text.slice(span[1]);
101
+ writeFileSync(t.path, out);
102
+ return { status: 'written', where: t.path };
103
+ }
104
+ mkdirSync(dirname(t.path), { recursive: true });
105
+ const sep = !text ? '' : text.endsWith('\n\n') ? '' : text.endsWith('\n') ? '\n' : '\n\n';
106
+ writeFileSync(t.path, text + sep + block);
107
+ return { status: 'written', where: t.path };
108
+ }
109
+ export function removeToml(t) {
110
+ if (!existsSync(t.path))
111
+ return { status: 'absent', where: t.path };
112
+ const text = readFileSync(t.path, 'utf8');
113
+ const span = tomlSpan(text, t.table);
114
+ if (!span)
115
+ return { status: 'absent', where: t.path };
116
+ writeFileSync(t.path, (text.slice(0, span[0]) + text.slice(span[1])).replace(/\n{3,}/g, '\n\n').replace(/\n+$/, '\n'));
117
+ return { status: 'removed', where: t.path };
118
+ }
119
+ export function hasToml(t) {
120
+ if (!existsSync(t.path))
121
+ return false;
122
+ const text = readFileSync(t.path, 'utf8');
123
+ const span = tomlSpan(text, t.table);
124
+ return !!span && text.slice(span[0], span[1]).trim() === tomlBlock(t).trim();
125
+ }
126
+ // ── commands ────────────────────────────────────────────────────────────────
127
+ export function runCommand(bin, args) {
128
+ const r = spawnSync(bin, args, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
129
+ const where = `${bin} ${args.join(' ')}`;
130
+ if (r.error)
131
+ return { status: 'failed', where, why: r.error.message };
132
+ if (r.status !== 0)
133
+ return { status: 'failed', where, why: (r.stderr || r.stdout || `exit ${r.status}`).trim().split('\n').slice(-3).join(' ') };
134
+ return { status: 'ran', where };
135
+ }
136
+ // ── one door for the three kinds ────────────────────────────────────────────
137
+ export function apply(t) {
138
+ switch (t.kind) {
139
+ case 'json': return applyJson(t);
140
+ case 'toml': return applyToml(t);
141
+ case 'command': return runCommand(t.bin, t.add);
142
+ case 'link': return { status: 'by-hand', where: t.href, why: t.note, snippet: '' };
143
+ }
144
+ }
145
+ export function remove(t) {
146
+ switch (t.kind) {
147
+ case 'json': return removeJson(t);
148
+ case 'toml': return removeToml(t);
149
+ case 'command': return runCommand(t.bin, t.remove);
150
+ case 'link': return { status: 'absent', where: t.href };
151
+ }
152
+ }
153
+ /** Present, absent, or unknowable (a command's own store, or a file with comments). */
154
+ export function has(t) {
155
+ switch (t.kind) {
156
+ case 'json': {
157
+ const r = hasJson(t);
158
+ return r === 'unreadable' ? 'unknown' : r;
159
+ }
160
+ case 'toml': return hasToml(t);
161
+ case 'command': return 'unknown';
162
+ case 'link': return 'unknown';
163
+ }
164
+ }
package/package.json CHANGED
@@ -1,6 +1,28 @@
1
1
  {
2
2
  "name": "sekura",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "Connect your coding tool to Sekura in one command: npx sekura@latest init",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "homepage": "https://sekura.ai/connect/",
8
+ "repository": { "type": "git", "url": "git+https://github.com/sekuraai/sekura-cli.git" },
9
+ "bugs": { "url": "https://github.com/sekuraai/sekura-cli/issues" },
10
+ "author": "Sekura Inc. <hello@sekura.ai>",
11
+ "keywords": ["sekura", "mcp", "security", "vulnerability-scanning", "claude-code", "cursor", "copilot", "codex"],
12
+ "bin": { "sekura": "dist/cli.js" },
13
+ "files": ["dist", "skills", "README.md", "LICENSE"],
14
+ "engines": { "node": ">=18" },
15
+ "scripts": {
16
+ "build": "tsc -p tsconfig.json && node -e \"require('fs').chmodSync('dist/cli.js', 0o755)\"",
17
+ "test": "npm run build && node --test test/init.test.mjs",
18
+ "prepack": "npm run build"
19
+ },
20
+ "dependencies": {
21
+ "@inquirer/prompts": "^7.8.0",
22
+ "commander": "^13.1.0"
23
+ },
24
+ "devDependencies": {
25
+ "@types/node": "^22.0.0",
26
+ "typescript": "^5.6.0"
27
+ }
28
+ }
@@ -0,0 +1,51 @@
1
+ ---
2
+ name: sekura
3
+ description: Use when the user wants a GitHub repository scanned for vulnerabilities, wants to read scan findings or a report, or wants to put a repository on a recurring Sekura scan. Covers the sekura MCP tools and the rule that every scan and plan is quoted first and started only after the user agrees.
4
+ ---
5
+
6
+ # Sekura
7
+
8
+ Sekura is an AI red team as a service. It scans a GitHub repository the user has
9
+ granted it, exploits what it finds to prove each finding, and delivers a report.
10
+ The `sekura` MCP server (https://sekura.ai/mcp) exposes it to this tool. Signing in is the
11
+ user's GitHub login on sekura.ai: if a tool answers that the user is signed out,
12
+ tell them to run `/mcp`, choose `sekura`, and approve in the browser.
13
+
14
+ ## Tools
15
+
16
+ Read, free, no confirmation:
17
+
18
+ - `getAccount`: who is signed in, what is billed, and whether the account's free first scan is still available on sekura.ai.
19
+ - `listRepositories`: the repositories the Sekura GitHub App is installed on.
20
+ - `listScans`, `getScan`, `getScanFindings`, `getReportLink`: scans, their status, their findings and the report.
21
+ - `getPlans`, `getBilling`: the repositories on a recurring plan, and a billing summary without card numbers.
22
+
23
+ Write, each in two steps:
24
+
25
+ - `startScan`: a Sekura Once scan of one repository.
26
+ - `startAlways`: a recurring Sekura Always plan for one repository.
27
+ - `runNow`: bring an Always plan's next scheduled run forward. Charges nothing.
28
+ - `cancelScan`, `cancelPlan`: stop a scan, or end a plan.
29
+
30
+ ## The two-step rule
31
+
32
+ Every write tool answers first with a preview: what will run, against which
33
+ repository, under which authorization wording, and the price. Show that text to
34
+ the user as it stands, including the price and the authorization sentence.
35
+ Start nothing until the user has agreed in the conversation. Then call the same
36
+ tool again with the arguments the preview names (`confirmArguments` in the
37
+ JSON block). Never invent a confirmation argument, and never pass one before the
38
+ user has said yes.
39
+
40
+ A card is charged only after the user approves the quoted amount. If no card is
41
+ on file, the answer carries a Stripe Checkout link: give it to the user and stop;
42
+ the scan starts once they have paid on sekura.ai.
43
+
44
+ ## What to tell the user
45
+
46
+ - The free first scan is taken on sekura.ai, never through this tool: when `getAccount` says it is still available, tell the user they can start it themselves at https://sekura.ai/app/start, and that a scan started here is Sekura Once at the quoted price.
47
+ - A scan can take a while. Offer `getScan` to check on it rather than polling in a loop.
48
+ - Findings carry proof. When summarising, keep the evidence the finding gives; do not soften a severity.
49
+ - Sekura scans only repositories the user has granted through the Sekura GitHub App. To add one: https://sekura.ai/app.
50
+
51
+ Documentation for every tool and client: https://sekura.ai/connect/