clembot-doorman 0.1.0 โ 0.2.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/README.md +137 -13
- package/WALKTHROUGH.md +3 -2
- package/doorman/.claude/hooks/session-notify.sh +67 -0
- package/doorman/.claude-plugin/plugin.json +1 -1
- package/doorman/cli/audit.mjs +232 -0
- package/doorman/cli/doorman.mjs +108 -6
- package/doorman/cli/install.mjs +29 -2
- package/doorman/cli/notify.mjs +145 -0
- package/doorman/cli/schedule.mjs +128 -0
- package/doorman/commands/doorman.md +1 -2
- package/doorman/hooks/hooks.json +34 -3
- package/doorman/package.json +1 -1
- package/doorman/recipes/README.md +6 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,18 +1,31 @@
|
|
|
1
|
-
<
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="media/hero.png" alt="Clembot Doorman - The Package Manager and Security Gate for AI Agents" width="100%">
|
|
3
|
+
</p>
|
|
2
4
|
|
|
3
5
|
# Clembot Doorman
|
|
4
6
|
|
|
5
|
-
> **The Package Manager & Security Doorman for
|
|
7
|
+
> **The Package Manager & Security Doorman for AI Agents.**
|
|
6
8
|
> Inspect your build. Recommend vetted MCPs from prompt history. Block rogue tools before they reach context.
|
|
7
9
|
|
|
8
10
|
[](https://clembot-doorman.wanessalabs.com)
|
|
9
11
|
[](https://clembot-doorman.bazgateway.com)
|
|
10
|
-
[](RUNBOOK.md)
|
|
11
13
|
[](LICENSE)
|
|
12
14
|
|
|
13
15
|
---
|
|
14
16
|
|
|
15
|
-
|
|
17
|
+
### ๐ ETHOnline 2026 Prize Tracks & Judge Quick-Links
|
|
18
|
+
|
|
19
|
+
| Track | Prize | Artifact / Manifest | What It Demonstrates |
|
|
20
|
+
|---|---|---|---|
|
|
21
|
+
| **Bazantic ยท Agentify an API** | $500 / 300 / 200 | [`01-vet-mcp-candidate.json`](./bazantic/recipes/01-vet-mcp-candidate.json) | Agentifies our live OpenAPI 3.1 `mcp-scorecard` service into an autonomous pre-flight tool that evaluates untrusted MCP servers before adoption. |
|
|
22
|
+
| **Bazantic ยท Continuity** | 2ร$500 | [`02-deepwiki-pinned-researcher.json`](./bazantic/recipes/02-deepwiki-pinned-researcher.json) | **87.6% Context Token Reduction**: Crushes 3,400 tokens of raw DeepWiki schema bloat down to 420 tokens using Doorman's cached pinned recipe. |
|
|
23
|
+
| **Bazantic ยท Best Multi-API Recipe** | $500 / 300 / 200 | [`03-governance-pipeline.json`](./bazantic/recipes/03-governance-pipeline.json) | **3-Gateway Autonomous Pipeline**: Audits tool safety โ Settles $0.01 audit micropayment on Base mainnet via x402 โ Pins SHA-256 hash to allowlist. |
|
|
24
|
+
| **Base ยท x402 Micropayments** | Sponsor Track | [`mcp-scorecard/src/routes/spend.ts`](./mcp-scorecard/src/routes/spend.ts) | Real $0.01 per-audit micropayment settlement using EIP-712 spend permits on Base mainnet. |
|
|
25
|
+
|
|
26
|
+
> **Judge Portals**: [Live Interactive Simulator](https://clembot-doorman.wanessalabs.com/#flow) ยท [Bazantic Showcase Page](https://clembot-doorman.wanessalabs.com/bazantic.html#recipes) ยท [ETHOnline Judging Criteria](https://clembot-doorman.wanessalabs.com/judges.html) ยท [2-Minute Verification Runbook](RUNBOOK.md)
|
|
27
|
+
|
|
28
|
+
---
|
|
16
29
|
|
|
17
30
|
**Clembot is built in many versions, with the latest utilizing Claude Code and a custom combination of agent harnesses.** While AI agents need tools to do real work, loading raw MCP servers blindly introduces **in-context steering ads**, prompt injection risks, and token-draining schema hallucination.
|
|
18
31
|
|
|
@@ -23,12 +36,62 @@
|
|
|
23
36
|
3. **Arm with Bazantic Recipes (`recipe.md`)**
|
|
24
37
|
Turns raw, unpredictable APIs into bounded, high-performing tools via structured `recipe.md` guidance. Built and verified against the Bazantic gateway (`clembot-doorman.bazgateway.com`).
|
|
25
38
|
|
|
26
|
-

|
|
27
40
|
|
|
28
41
|
*A real run against a real server. The grade, the hard fail, and the arithmetic are live values, not a mockup: `webzum.com/api/mcp` scores **89.9% on configuration** and still fails, because one tool description injects **6,290 characters of unprompted upsell and competitor steering** directly into your agent's context window. One cent through the Bazantic gateway answered a question that would have cost $54.47 to measure in an unguided LLM loop. [Run it live in the simulator](https://clembot-doorman.wanessalabs.com/#flow).*
|
|
29
42
|
|
|
30
43
|
---
|
|
31
44
|
|
|
45
|
+
## ๐ธ Architecture & Platform Gallery
|
|
46
|
+
|
|
47
|
+
<table>
|
|
48
|
+
<tr>
|
|
49
|
+
<td width="50%" align="center" valign="top">
|
|
50
|
+
<a href="media/gallery-03-two-phase-solution.png"><img src="media/gallery-03-two-phase-solution.png" alt="2 phase solution with Clembot Doorman" width="100%"></a><br>
|
|
51
|
+
<b>2-Phase Defense Solution with Clembot Doorman</b><br>
|
|
52
|
+
<i>$0.00 local pre-screening before $0.01 Base mainnet verification</i>
|
|
53
|
+
</td>
|
|
54
|
+
<td width="50%" align="center" valign="top">
|
|
55
|
+
<a href="media/gallery-04-tiered-architecture.png"><img src="media/gallery-04-tiered-architecture.png" alt="Tiered Clembot-doorman Architecture" width="100%"></a><br>
|
|
56
|
+
<b>Tiered Clembot-Doorman Architecture</b><br>
|
|
57
|
+
<i>Four modular layers (L0 Doctor โ L1 Needs โ L2 Offline Gate โ L3 Scorecard)</i>
|
|
58
|
+
</td>
|
|
59
|
+
</tr>
|
|
60
|
+
<tr>
|
|
61
|
+
<td width="50%" align="center" valign="top">
|
|
62
|
+
<a href="media/gallery-01-bazantic-gateway.png"><img src="media/gallery-01-bazantic-gateway.png" alt="Bazantic doorman gateway" width="100%"></a><br>
|
|
63
|
+
<b>Bazantic Doorman Gateway</b><br>
|
|
64
|
+
<i>Live edge gateway (<code>clembot-doorman.bazgateway.com</code>) with 10 managed tools</i>
|
|
65
|
+
</td>
|
|
66
|
+
<td width="50%" align="center" valign="top">
|
|
67
|
+
<a href="media/gallery-02-bazantic-recipes.png"><img src="media/gallery-02-bazantic-recipes.png" alt="Bazantic recipes" width="100%"></a><br>
|
|
68
|
+
<b>Bazantic Recipes</b><br>
|
|
69
|
+
<i>Three published production recipes delivering 87% context token reduction</i>
|
|
70
|
+
</td>
|
|
71
|
+
</tr>
|
|
72
|
+
<tr>
|
|
73
|
+
<td width="50%" align="center" valign="top">
|
|
74
|
+
<a href="media/gallery-05-powered-by-bazantic.png"><img src="media/gallery-05-powered-by-bazantic.png" alt="Powered by Bazantic" width="100%"></a><br>
|
|
75
|
+
<b>Powered by Bazantic</b><br>
|
|
76
|
+
<i>Deep technical integration: Continuity, Edge Infrastructure, and Cryptographic Allowlisting</i>
|
|
77
|
+
</td>
|
|
78
|
+
<td width="50%" align="center" valign="top">
|
|
79
|
+
<a href="media/gallery-06-doorman-demo.png"><img src="media/gallery-06-doorman-demo.png" alt="Doorman demo" width="100%"></a><br>
|
|
80
|
+
<b>Doorman Demo</b><br>
|
|
81
|
+
<i>Interactive flow testing real MCPs with toggleable Base x402 micropayments</i>
|
|
82
|
+
</td>
|
|
83
|
+
</tr>
|
|
84
|
+
<tr>
|
|
85
|
+
<td colspan="2" align="center" valign="top">
|
|
86
|
+
<a href="media/gallery-07-clembot-org-chart.png"><img src="media/gallery-07-clembot-org-chart.png" alt="Clembot org chart" width="100%"></a><br>
|
|
87
|
+
<b>Clembot Organization Chart</b><br>
|
|
88
|
+
<i>27 autonomous agents across 5 divisions: Design Director is the sole surface gated by Doorman</i>
|
|
89
|
+
</td>
|
|
90
|
+
</tr>
|
|
91
|
+
</table>
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
32
95
|
## What this is for
|
|
33
96
|
|
|
34
97
|
Doorman measures whether a candidate tool actually helps **your** agent, and gives you a report about **your** build.
|
|
@@ -66,6 +129,7 @@ The verdict is yours, produced on your machine, from numbers we never receive.
|
|
|
66
129
|
| `site/` | The live product & explainer at [clembot-doorman.wanessalabs.com](https://clembot-doorman.wanessalabs.com). |
|
|
67
130
|
| `fixtures/planted-bad-mcp/` | A deliberately hostile MCP server, deployed, so the demo denies something real instead of a line in a JSON file. |
|
|
68
131
|
|
|
132
|
+
<a id="install"></a>
|
|
69
133
|
## Quickstart & Installation
|
|
70
134
|
|
|
71
135
|
**The answer depends on your stack, so run it on yours.**
|
|
@@ -106,20 +170,25 @@ components that never register:
|
|
|
106
170
|
claude plugin details clembot-doorman
|
|
107
171
|
# Skills (3) doorman, doorman-guide, vet
|
|
108
172
|
# Agents (1) doorman
|
|
109
|
-
# Hooks (
|
|
173
|
+
# Hooks (2) PreToolUse, SessionStart
|
|
110
174
|
# MCP servers (1) scorecard
|
|
111
175
|
```
|
|
112
176
|
|
|
113
|
-
### 2. The CLI: doctor, needs, report, watch, eval
|
|
177
|
+
### 2. The CLI: doctor, needs, report, watch, notify, eval
|
|
114
178
|
|
|
115
179
|
```bash
|
|
116
|
-
|
|
117
|
-
npm i -g ./clembot-doorman
|
|
180
|
+
npm i -g clembot-doorman
|
|
118
181
|
doorman --version
|
|
119
182
|
```
|
|
120
183
|
|
|
121
184
|
Node 20+. Zero runtime dependencies, deliberately: every dependency is one more thing that can fail to install on your machine.
|
|
122
185
|
|
|
186
|
+
**`notify` and the SessionStart hook need 0.2.0 or later.** The 0.1.0 on npm was
|
|
187
|
+
published before they landed, so `doorman --version` reporting `0.1.0` means the
|
|
188
|
+
push half is absent and no session will ever announce anything. Nothing errors:
|
|
189
|
+
that build simply stays quiet forever, which is indistinguishable from having no
|
|
190
|
+
news.
|
|
191
|
+
|
|
123
192
|
**New here?** [`WALKTHROUGH.md`](WALKTHROUGH.md) runs the first ten minutes
|
|
124
193
|
against three real builds: one with a long history, one brand new, and one in
|
|
125
194
|
between. The output in it is captured from real runs, not written by hand.
|
|
@@ -320,6 +389,54 @@ Doorman is specifically designed to stop "skill sprawl" and prevent duplicate to
|
|
|
320
389
|
2. **Adverse Drift Detection (`doorman watch`)**:
|
|
321
390
|
When you run `doorman watch`, any candidate server already in your inventory is tagged `already-installed`. If an installed server is downgraded or caught with prompt injection on the feed, `watch` raises an immediate security alert.
|
|
322
391
|
|
|
392
|
+
2a. **Push, without a daemon and without telemetry**:
|
|
393
|
+
Nobody can push to a laptop behind NAT that is asleep half the day, so the
|
|
394
|
+
push here is not a new transport. It is the poll going invisible. A
|
|
395
|
+
SessionStart hook prints a digest that is already on disk, then fires a
|
|
396
|
+
detached refresh so the next session is current. The hook makes no network
|
|
397
|
+
call: one that waited on a fetch would make every session start as slow as
|
|
398
|
+
the worst network it has seen, and offline would make them all fail.
|
|
399
|
+
|
|
400
|
+
```text
|
|
401
|
+
## doorman
|
|
402
|
+
|
|
403
|
+
1 newly graded server this build does not have:
|
|
404
|
+
- **A** 85.71/100, model not recorded https://mcp.deepwiki.com/mcp
|
|
405
|
+
not measured: behavioral, guidance
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
Three rules, and each one is a notification product failing if broken. It
|
|
409
|
+
is **silent when nothing is new**, because a hook that reports "nothing new"
|
|
410
|
+
every morning teaches you to skip past the one morning it matters. It
|
|
411
|
+
**announces nothing on the first run**, because with no cursor the feed
|
|
412
|
+
returns everything graded so far and 26 rows is a catalogue, not news. And a
|
|
413
|
+
digest is **shown exactly once**, because the same three servers every
|
|
414
|
+
morning is how a notification becomes furniture.
|
|
415
|
+
|
|
416
|
+
2b. **Popularity and trend, as a second axis (`GET /feed`, `?sort=trending`)**:
|
|
417
|
+
Every feed row carries a `popularity` block: Smithery use counts, npm weekly
|
|
418
|
+
downloads and GitHub stars, swept daily, with a median percentile and a
|
|
419
|
+
trend.
|
|
420
|
+
|
|
421
|
+
It is **never part of the score**. The grade is what happened when an agent
|
|
422
|
+
drove the server; popularity is how many people installed it without asking
|
|
423
|
+
that. A popular F is the most useful row this feed can publish, and a
|
|
424
|
+
blended number is the one thing guaranteed to bury it.
|
|
425
|
+
|
|
426
|
+
Counts are **ranked within each source and never summed across them**:
|
|
427
|
+
87,579 Smithery uses, 4,200 npm downloads and 1,100 stars are three units
|
|
428
|
+
counting three populations, and adding them makes a meaningless number that
|
|
429
|
+
still sorts confidently. `sources_measured` says how many sources backed the
|
|
430
|
+
percentile, because a server ranked on one and a server ranked on three are
|
|
431
|
+
not equally known. A trend needs two readings at least 12 hours apart, so a
|
|
432
|
+
newly tracked server reports `null` rather than zero growth, and a source
|
|
433
|
+
that could not be read is **absent rather than zero**.
|
|
434
|
+
|
|
435
|
+
**Doorman's own install counts are refused as a fourth source.** They would
|
|
436
|
+
be the best popularity signal available to anyone, and collecting them needs
|
|
437
|
+
telemetry. That would sell the guarantee that makes `watch` and `needs`
|
|
438
|
+
worth running at all: your inventory never leaves your machine.
|
|
439
|
+
|
|
323
440
|
3. **Frontmatter Arithmetic (30 KB vs 640 KB)**:
|
|
324
441
|
Doorman reads only YAML frontmatter (`name`, `description`) from `.claude/skills/*/SKILL.md` and `.claude/agents/*.md`. In our production vault, reading full markdown bodies was **642 KB**; reading frontmatter was **30 KB**. This allows the complete roster to be reviewed by a model in a single prompt without bloating context.
|
|
325
442
|
|
|
@@ -645,13 +762,20 @@ node runner/run.mjs --once --server https://mcp.deepwiki.com/mcp \
|
|
|
645
762
|
--needed-for "look up how a public repository works" \
|
|
646
763
|
--static-only --out ../evidence/deepwiki
|
|
647
764
|
|
|
648
|
-
# Full behavioural run (needs a key)
|
|
649
|
-
|
|
765
|
+
# Full behavioural run (needs a key).
|
|
766
|
+
# `=...` is NOT a value: read the secret in rather than pasting a placeholder,
|
|
767
|
+
# which also keeps it out of shell history. A pasted "..." reaches the server
|
|
768
|
+
# as a wrong token and comes back 401, which reads as a broken credential
|
|
769
|
+
# rather than as a placeholder nobody substituted.
|
|
770
|
+
read -rsp 'ANTHROPIC_API_KEY: ' ANTHROPIC_API_KEY && export ANTHROPIC_API_KEY
|
|
771
|
+
# --once PRINTS ONLY. Add --out DIR to keep the evidence bundle, or queue the
|
|
772
|
+
# audit and use --poll below to publish it to the feed.
|
|
650
773
|
node runner/run.mjs --once --server https://mcp.deepwiki.com/mcp \
|
|
651
|
-
--needed-for "look up how a public repository works"
|
|
774
|
+
--needed-for "look up how a public repository works" --out out/deepwiki
|
|
652
775
|
|
|
653
776
|
# Poll the queue
|
|
654
|
-
export RUNNER_TOKEN
|
|
777
|
+
read -rsp 'RUNNER_TOKEN: ' RUNNER_TOKEN && export RUNNER_TOKEN
|
|
778
|
+
export SCORECARD_API=http://127.0.0.1:8799
|
|
655
779
|
node runner/run.mjs --poll
|
|
656
780
|
```
|
|
657
781
|
|
package/WALKTHROUGH.md
CHANGED
|
@@ -5,6 +5,8 @@ captured from an actual run on 2026-09-11 against the live feed, not written by
|
|
|
5
5
|
hand. Where a run produced nothing useful, that is shown too, because "nothing
|
|
6
6
|
matched" is a result and hiding it would make this document a brochure.
|
|
7
7
|
|
|
8
|
+

|
|
9
|
+
|
|
8
10
|
---
|
|
9
11
|
|
|
10
12
|
## Install both halves
|
|
@@ -13,8 +15,7 @@ matched" is a result and hiding it would make this document a brochure.
|
|
|
13
15
|
claude plugin marketplace add clemenswan/clembot-doorman
|
|
14
16
|
claude plugin install clembot-doorman # the gate, /doorman, /vet, the subagent
|
|
15
17
|
|
|
16
|
-
|
|
17
|
-
npm i -g ./clembot-doorman # doctor, needs, report, watch, eval
|
|
18
|
+
npm i -g clembot-doorman # doctor, needs, report, watch, eval
|
|
18
19
|
```
|
|
19
20
|
|
|
20
21
|
Check what actually loaded. A manifest can validate and still ship components
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# SessionStart hook. The visible half of the push model.
|
|
4
|
+
#
|
|
5
|
+
# Prints a digest that is ALREADY ON DISK, then fires a detached refresh so the
|
|
6
|
+
# next session is current. It makes no network call itself, which is the whole
|
|
7
|
+
# design: a SessionStart hook that waits on a fetch makes every session start as
|
|
8
|
+
# slow as the worst network it has ever seen, and offline it makes them fail.
|
|
9
|
+
# The cost of this hook on a normal morning is one `test -f` and one `cat`.
|
|
10
|
+
#
|
|
11
|
+
# THIS IS NOT THE GATE. `mcp-gate.sh` is a security control and is offline,
|
|
12
|
+
# dependency-free, and fails CLOSED with exit 2. This is a notification and
|
|
13
|
+
# fails OPEN and silent: it always exits 0, and every failure path prints
|
|
14
|
+
# nothing. A notifier that can break a session start is worse than no notifier.
|
|
15
|
+
#
|
|
16
|
+
# ponytail: the refresh rides on session start, so the first session after
|
|
17
|
+
# install prints nothing (no digest yet) and news is at most one session stale.
|
|
18
|
+
# A background poller (`doorman watch --poll`, roadmap Phase 2) would close that
|
|
19
|
+
# gap and is not worth a daemon yet.
|
|
20
|
+
|
|
21
|
+
set -uo pipefail
|
|
22
|
+
|
|
23
|
+
# Never let this hook be the reason a session fails to start.
|
|
24
|
+
trap 'exit 0' ERR
|
|
25
|
+
|
|
26
|
+
PROJECT_DIR="${CLAUDE_PROJECT_DIR:-$PWD}"
|
|
27
|
+
DIGEST="$PROJECT_DIR/.doorman/notify.md"
|
|
28
|
+
|
|
29
|
+
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
30
|
+
RESOLVE="$HERE/../../scripts/resolve-cli.sh"
|
|
31
|
+
|
|
32
|
+
# 1. Say what is already known. Reading is destructive: consume-once is what
|
|
33
|
+
# stops the same three servers being announced every morning until they are
|
|
34
|
+
# furniture. `notify consume` does the delete.
|
|
35
|
+
if [ -f "$DIGEST" ]; then
|
|
36
|
+
CLI=""
|
|
37
|
+
[ -x "$RESOLVE" ] && CLI="$(bash "$RESOLVE" 2>/dev/null || true)"
|
|
38
|
+
if [ -n "$CLI" ] && [ "$CLI" != "NOT_FOUND" ]; then
|
|
39
|
+
$CLI notify consume --digest "$DIGEST" 2>/dev/null || true
|
|
40
|
+
else
|
|
41
|
+
# No CLI resolved. Print it anyway and remove it by hand rather than
|
|
42
|
+
# holding news hostage to a resolution problem.
|
|
43
|
+
cat "$DIGEST" 2>/dev/null || true
|
|
44
|
+
rm -f "$DIGEST" 2>/dev/null || true
|
|
45
|
+
fi
|
|
46
|
+
fi
|
|
47
|
+
|
|
48
|
+
# 2. Refresh for next time, detached.
|
|
49
|
+
#
|
|
50
|
+
# stdout and stderr MUST be redirected away from this process. A background
|
|
51
|
+
# child that inherits the hook's stdout keeps the pipe open, and the harness
|
|
52
|
+
# waits on the pipe rather than on the process, so an unredirected `&` turns a
|
|
53
|
+
# fire-and-forget into the exact session-start stall this design avoids.
|
|
54
|
+
if [ -z "${DOORMAN_NO_REFRESH:-}" ]; then
|
|
55
|
+
CLI="${CLI:-}"
|
|
56
|
+
if [ -z "$CLI" ] && [ -x "$RESOLVE" ]; then
|
|
57
|
+
CLI="$(bash "$RESOLVE" 2>/dev/null || true)"
|
|
58
|
+
fi
|
|
59
|
+
if [ -n "$CLI" ] && [ "$CLI" != "NOT_FOUND" ]; then
|
|
60
|
+
LOG="$PROJECT_DIR/.doorman/notify.log"
|
|
61
|
+
mkdir -p "$PROJECT_DIR/.doorman" 2>/dev/null || true
|
|
62
|
+
nohup $CLI notify refresh --root "$PROJECT_DIR" >"$LOG" 2>&1 &
|
|
63
|
+
disown 2>/dev/null || true
|
|
64
|
+
fi
|
|
65
|
+
fi
|
|
66
|
+
|
|
67
|
+
exit 0
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://anthropic.com/claude-code/plugin.schema.json",
|
|
3
3
|
"name": "clembot-doorman",
|
|
4
|
-
"version": "0.1
|
|
4
|
+
"version": "0.2.1",
|
|
5
5
|
"description": "A gate on what your agent is allowed to use. Blocks MCP servers that are not on your trust list, reads your prompt history to find what the build actually needs, and grades candidates before you install them.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Clemens Wan",
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `doorman audit` โ Unified security audit & tool recommendation report.
|
|
3
|
+
*
|
|
4
|
+
* Combines three layers in one fast, local sweep:
|
|
5
|
+
* 1. L0 doctor: Inspects your harness, gate wiring, and installed MCP servers.
|
|
6
|
+
* 2. L0.5 needs: Scans local prompt history to detect missing capability needs.
|
|
7
|
+
* 3. L2 watch: Cross-references unmet gaps against the public graded feed.
|
|
8
|
+
*
|
|
9
|
+
* Output: An actionable, executive-grade posture and tool recommendation report.
|
|
10
|
+
* Free, offline-first, zero runtime dependencies.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { resolve } from 'node:path';
|
|
14
|
+
import { writeFileSync } from 'node:fs';
|
|
15
|
+
import { doctor } from './doctor.mjs';
|
|
16
|
+
import { needs as readNeeds } from './needs.mjs';
|
|
17
|
+
import { watch } from './watch.mjs';
|
|
18
|
+
|
|
19
|
+
export async function auditProject(targetDir = process.cwd(), opts = {}) {
|
|
20
|
+
const absRoot = resolve(targetDir);
|
|
21
|
+
|
|
22
|
+
// 1. Run Doctor (L0: Harness, Gate, Installed servers)
|
|
23
|
+
const doc = await doctor(absRoot, opts);
|
|
24
|
+
if (!doc.ok) {
|
|
25
|
+
return { ok: false, why: doc.why };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// 2. Run Needs (L0.5: Prompt history scan for capability gaps)
|
|
29
|
+
let needsResult = null;
|
|
30
|
+
try {
|
|
31
|
+
needsResult = await readNeeds({
|
|
32
|
+
root: absRoot,
|
|
33
|
+
historyDir: opts.historyDir,
|
|
34
|
+
candidateFile: opts.candidateFile,
|
|
35
|
+
api: opts.api,
|
|
36
|
+
});
|
|
37
|
+
} catch {
|
|
38
|
+
needsResult = null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// 3. Run Watch (L2: Classified feed of newly graded tools & threats)
|
|
42
|
+
let watchResult = null;
|
|
43
|
+
try {
|
|
44
|
+
watchResult = await watch({
|
|
45
|
+
root: absRoot,
|
|
46
|
+
api: opts.api,
|
|
47
|
+
all: true,
|
|
48
|
+
});
|
|
49
|
+
} catch {
|
|
50
|
+
watchResult = null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Synthesize executive findings
|
|
54
|
+
const gateStatus = doc.gate?.verdict || 'unknown';
|
|
55
|
+
const isGateWired = gateStatus === 'installed and wired' || gateStatus.includes('wired');
|
|
56
|
+
const harnesses = (doc.harnesses || []).map((h) => h.harness);
|
|
57
|
+
const installedCount = (doc.servers || []).length;
|
|
58
|
+
|
|
59
|
+
const gaps = (needsResult?.matches || []).filter((m) => m.status === 'GAP' || m.status === 'UNMET');
|
|
60
|
+
const covered = (needsResult?.matches || []).filter((m) => m.status === 'COVERED');
|
|
61
|
+
|
|
62
|
+
const candidates = watchResult?.candidates || [];
|
|
63
|
+
const recommended = candidates
|
|
64
|
+
.filter((c) => (c.grade === 'A' || c.grade === 'B') && c.verdict === 'unreviewed' && !c.is_fixture && !c.self_graded)
|
|
65
|
+
.slice(0, 5);
|
|
66
|
+
|
|
67
|
+
const blockedThreats = candidates
|
|
68
|
+
.filter((c) => c.grade === 'F' || c.hard_fail || c.verdict === 'blocked')
|
|
69
|
+
.slice(0, 3);
|
|
70
|
+
|
|
71
|
+
const result = {
|
|
72
|
+
ok: true,
|
|
73
|
+
root: absRoot,
|
|
74
|
+
timestamp: new Date().toISOString(),
|
|
75
|
+
posture: {
|
|
76
|
+
gateStatus,
|
|
77
|
+
isGateWired,
|
|
78
|
+
harnesses,
|
|
79
|
+
installedCount,
|
|
80
|
+
agentsCount: doc.agents?.count ?? 0,
|
|
81
|
+
},
|
|
82
|
+
needs: {
|
|
83
|
+
totalPrompts: needsResult?.promptCount ?? 0,
|
|
84
|
+
gaps,
|
|
85
|
+
covered,
|
|
86
|
+
},
|
|
87
|
+
recommendations: recommended,
|
|
88
|
+
threats: blockedThreats,
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
if (opts.out) {
|
|
92
|
+
const md = renderAuditMarkdown(result);
|
|
93
|
+
writeFileSync(opts.out, md, 'utf8');
|
|
94
|
+
result.outFile = opts.out;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return result;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function renderAuditMarkdown(res) {
|
|
101
|
+
const lines = [];
|
|
102
|
+
lines.push('# Clembot Doorman Security & Tool Recommendation Report');
|
|
103
|
+
lines.push(`\n**Target:** \`${res.root}\``);
|
|
104
|
+
lines.push(`**Generated:** ${res.timestamp}`);
|
|
105
|
+
lines.push('');
|
|
106
|
+
|
|
107
|
+
// Posture
|
|
108
|
+
lines.push('## 1. Security & Gate Posture');
|
|
109
|
+
const gateIcon = res.posture.isGateWired ? 'PASS' : 'WARN';
|
|
110
|
+
lines.push(`- **Gate Status:** [${gateIcon}] ${res.posture.gateStatus}`);
|
|
111
|
+
lines.push(`- **Harnesses Detected:** ${res.posture.harnesses.join(', ') || 'none'}`);
|
|
112
|
+
lines.push(`- **Reachable MCP Servers:** ${res.posture.installedCount}`);
|
|
113
|
+
lines.push(`- **Subagents Configured:** ${res.posture.agentsCount}`);
|
|
114
|
+
lines.push('');
|
|
115
|
+
|
|
116
|
+
// Recommendations based on needs
|
|
117
|
+
lines.push('## 2. Capability Needs & Tool Recommendations');
|
|
118
|
+
if (res.needs.gaps.length > 0) {
|
|
119
|
+
lines.push('Your prompt history reveals the following unmet tool capabilities:');
|
|
120
|
+
for (const g of res.needs.gaps) {
|
|
121
|
+
lines.push(`- **${g.title}:** ${g.promptsCount} prompt(s) reaching for this capability.`);
|
|
122
|
+
if (g.topCandidates && g.topCandidates.length > 0) {
|
|
123
|
+
for (const c of g.topCandidates.slice(0, 2)) {
|
|
124
|
+
lines.push(` โ Recommended: **${c.name || c.url}** (${c.grade ? `Grade ${c.grade}` : 'verified'})`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
} else {
|
|
129
|
+
lines.push('- No active capability gaps detected in prompt history.');
|
|
130
|
+
}
|
|
131
|
+
lines.push('');
|
|
132
|
+
|
|
133
|
+
if (res.recommendations.length > 0) {
|
|
134
|
+
lines.push('### Top Verified Safe MCP Servers');
|
|
135
|
+
for (const r of res.recommendations) {
|
|
136
|
+
const score = typeof r.score === 'number' ? `${r.score.toFixed(1)}/100` : 'passing';
|
|
137
|
+
lines.push(`- **${r.server_name || r.server_url}** โ Grade **${r.grade}** (${score})`);
|
|
138
|
+
lines.push(` \`${r.server_url}\``);
|
|
139
|
+
}
|
|
140
|
+
lines.push('');
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Threats
|
|
144
|
+
lines.push('## 3. Threat Intelligence');
|
|
145
|
+
if (res.threats.length > 0) {
|
|
146
|
+
lines.push('The following servers failed security inspection and are blocked at the gate:');
|
|
147
|
+
for (const t of res.threats) {
|
|
148
|
+
const reason = t.hard_fail || 'Failed safety scan / commercial steering detected';
|
|
149
|
+
lines.push(`- โ ๏ธ **${t.server_name || t.server_url}** โ Grade **${t.grade}** [BLOCKED]: ${reason}`);
|
|
150
|
+
}
|
|
151
|
+
} else {
|
|
152
|
+
lines.push('- No active threat flags recorded on your trust list.');
|
|
153
|
+
}
|
|
154
|
+
lines.push('');
|
|
155
|
+
|
|
156
|
+
// Next Steps
|
|
157
|
+
lines.push('## 4. Recommended Actions');
|
|
158
|
+
if (!res.posture.isGateWired) {
|
|
159
|
+
lines.push('1. **Wire the security gate:** `claude plugin marketplace add clemenswan/clembot-doorman && claude plugin install clembot-doorman`');
|
|
160
|
+
}
|
|
161
|
+
lines.push('2. **Vet new candidate servers before adoption:** `/vet <url>`');
|
|
162
|
+
lines.push('3. **Allow trusted servers:** `doorman allow <server-name>`');
|
|
163
|
+
lines.push('4. **Schedule recurring audits:** `doorman schedule`');
|
|
164
|
+
|
|
165
|
+
return lines.join('\n');
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function renderAudit(res) {
|
|
169
|
+
const lines = [];
|
|
170
|
+
lines.push('');
|
|
171
|
+
lines.push('================================================================');
|
|
172
|
+
lines.push(' CLEMBOT DOORMAN ยท UNIFIED AUDIT & RECOMMENDATIONS ');
|
|
173
|
+
lines.push('================================================================');
|
|
174
|
+
lines.push(`Build: ${res.root}`);
|
|
175
|
+
lines.push('');
|
|
176
|
+
|
|
177
|
+
// 1. Security Posture
|
|
178
|
+
lines.push('โโ 1. SECURITY POSTURE โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ');
|
|
179
|
+
const gateMark = res.posture.isGateWired ? 'โ' : '!';
|
|
180
|
+
lines.push(` [${gateMark}] Gate: ${res.posture.gateStatus}`);
|
|
181
|
+
lines.push(` [i] Harness: ${res.posture.harnesses.join(', ') || 'None declared'}`);
|
|
182
|
+
lines.push(` [i] Reachable MCP Tools: ${res.posture.installedCount} declared`);
|
|
183
|
+
lines.push('');
|
|
184
|
+
|
|
185
|
+
// 2. Capability Recommendations
|
|
186
|
+
lines.push('โโ 2. CAPABILITY GAPS & RECOMMENDED TOOLS โโโโโโโโโโโโโโโโโโโโโโ');
|
|
187
|
+
if (res.needs.gaps.length > 0) {
|
|
188
|
+
lines.push(` Detected ${res.needs.gaps.length} capability gap(s) from prompt history:`);
|
|
189
|
+
for (const g of res.needs.gaps) {
|
|
190
|
+
lines.push(` โข ${g.title} (${g.promptsCount} prompt asks)`);
|
|
191
|
+
}
|
|
192
|
+
} else {
|
|
193
|
+
lines.push(' No unmet capability gaps detected.');
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (res.recommendations.length > 0) {
|
|
197
|
+
lines.push('');
|
|
198
|
+
lines.push(' Top Vetted Candidates from Graded Feed:');
|
|
199
|
+
for (const r of res.recommendations) {
|
|
200
|
+
const score = typeof r.score === 'number' ? `${r.score.toFixed(1)}/100` : '';
|
|
201
|
+
lines.push(` โ [Grade ${r.grade} ยท ${score}] ${r.server_name || 'Server'}`);
|
|
202
|
+
lines.push(` URL: ${r.server_url}`);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
lines.push('');
|
|
206
|
+
|
|
207
|
+
// 3. Blocked Threats
|
|
208
|
+
lines.push('โโ 3. THREATS BLOCKED AT THE GATE โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ');
|
|
209
|
+
if (res.threats.length > 0) {
|
|
210
|
+
for (const t of res.threats) {
|
|
211
|
+
lines.push(` โ [Grade ${t.grade}] ${t.server_name || t.server_url}`);
|
|
212
|
+
if (t.hard_fail) lines.push(` Reason: ${t.hard_fail}`);
|
|
213
|
+
}
|
|
214
|
+
} else {
|
|
215
|
+
lines.push(' No active security alerts on installed servers.');
|
|
216
|
+
}
|
|
217
|
+
lines.push('');
|
|
218
|
+
|
|
219
|
+
// 4. Quick Actions
|
|
220
|
+
lines.push('โโ 4. QUICK ACTIONS โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ');
|
|
221
|
+
if (!res.posture.isGateWired) {
|
|
222
|
+
lines.push(' โข Wire Gate: claude plugin install clembot-doorman');
|
|
223
|
+
}
|
|
224
|
+
lines.push(' โข Vet Tool: /vet <candidate_url>');
|
|
225
|
+
lines.push(' โข Trust Tool: doorman allow <server_name>');
|
|
226
|
+
lines.push(' โข Schedule: doorman schedule');
|
|
227
|
+
if (res.outFile) {
|
|
228
|
+
lines.push(` โข Report: Saved to ${res.outFile}`);
|
|
229
|
+
}
|
|
230
|
+
lines.push('');
|
|
231
|
+
return lines.join('\n');
|
|
232
|
+
}
|
package/doorman/cli/doorman.mjs
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
doorman needs [path] read this build's own prompt history, propose servers.
|
|
17
17
|
* doorman watch [path] poll the feed for candidates new to THIS build.
|
|
18
18
|
* doorman discover sweep a public directory for candidates. Curate, never enqueue.
|
|
19
|
+
* doorman notify refresh|consume the SessionStart push. Called by the hook, not by hand.
|
|
19
20
|
*
|
|
20
21
|
* Zero runtime dependencies, per the requirement in package.json. That is why
|
|
21
22
|
* the YAML loader and the arg parser are in-tree rather than installed.
|
|
@@ -29,12 +30,49 @@ import { discover, renderDiscover, writeCandidates } from './discover.mjs';
|
|
|
29
30
|
import { needs as readNeeds, render as renderNeedsCli } from './needs.mjs';
|
|
30
31
|
import { install, renderInstall } from './install.mjs';
|
|
31
32
|
import { allow, renderAllow, SCOPES } from './allow.mjs';
|
|
32
|
-
|
|
33
|
-
|
|
33
|
+
import { refreshNotify, consumeDigest, DEFAULT_DIGEST } from './notify.mjs';
|
|
34
|
+
import { auditProject, renderAudit } from './audit.mjs';
|
|
35
|
+
import { schedule as scheduleReport, renderSchedule } from './schedule.mjs';
|
|
36
|
+
import { join } from 'node:path';
|
|
37
|
+
|
|
38
|
+
// Pinned to every other declaration by version.test.mjs. There are FOUR of
|
|
39
|
+
// them (root package.json, doorman/package.json, plugin.json, this) and this
|
|
40
|
+
// one silently reported 0.1.0 out of a 0.2.0 tarball.
|
|
41
|
+
const VERSION = '0.2.1';
|
|
42
|
+
|
|
43
|
+
const QUICKSTART = `
|
|
44
|
+
doorman ${VERSION} โ Security gate & tool package manager for AI agents
|
|
45
|
+
|
|
46
|
+
QUICKSTART (3 SIMPLE STEPS):
|
|
47
|
+
1. doorman audit Run full build check & get vetted MCP recommendations
|
|
48
|
+
2. /vet <url> Audit an untrusted candidate MCP server before adoption
|
|
49
|
+
3. doorman schedule Configure automated weekly/daily audit reports
|
|
50
|
+
|
|
51
|
+
COMMON COMMANDS:
|
|
52
|
+
doorman audit [path] Unified scan: doctor + prompt needs + recommended tools
|
|
53
|
+
doorman schedule Configure recurring automated reports (cron, GitHub Actions)
|
|
54
|
+
doorman doctor [path] L0 build & gate inspection (free, offline, <5ms)
|
|
55
|
+
doorman needs [path] L0.5 scan prompt history for missing capabilities
|
|
56
|
+
doorman watch [path] Check public feed for newly graded tools
|
|
57
|
+
doorman allow <server> Trust an MCP server by name on your local allowlist
|
|
58
|
+
doorman install [path] Install the gate & hooks into this project
|
|
59
|
+
|
|
60
|
+
Run \`doorman --help\` for full manual and advanced benchmark flags.
|
|
61
|
+
`;
|
|
34
62
|
|
|
35
63
|
const HELP = `
|
|
36
64
|
doorman ${VERSION} โ measure a candidate, do not just read it
|
|
37
65
|
|
|
66
|
+
doorman audit [path] [--out FILE] [--json]
|
|
67
|
+
Unified security audit & capability recommendations. Runs doctor,
|
|
68
|
+
prompt-history needs, and watch in one fast command. Produces an
|
|
69
|
+
executive report with security posture, capability gaps, and top
|
|
70
|
+
verified tools.
|
|
71
|
+
|
|
72
|
+
doorman schedule [path] [--github]
|
|
73
|
+
Set up automated recurring audit reports via GitHub Actions, system
|
|
74
|
+
cron, or session-notify hooks.
|
|
75
|
+
|
|
38
76
|
doorman doctor [path]
|
|
39
77
|
L0. What is in YOUR build: which harness, which MCP servers your agents can
|
|
40
78
|
reach, how many subagents hold MCP tools, and whether the gate is installed
|
|
@@ -58,6 +96,12 @@ doorman ${VERSION} โ measure a candidate, do not just read it
|
|
|
58
96
|
a match is only ever worth-measuring. Nothing here drove anything, so
|
|
59
97
|
nothing here claims a server will work. Only eval answers that.
|
|
60
98
|
|
|
99
|
+
doorman notify refresh [--root DIR] | doorman notify consume [--digest FILE]
|
|
100
|
+
The push half, and the SessionStart hook is what calls it. refresh polls
|
|
101
|
+
the feed and leaves a short digest on disk; consume prints that digest
|
|
102
|
+
and deletes it. Nothing is announced on the first run, nothing is written
|
|
103
|
+
when nothing is new, and a digest is shown exactly once.
|
|
104
|
+
|
|
61
105
|
doorman report <link> [--out DIR] [--needed-for TEXT]
|
|
62
106
|
L1. The static implementation report: protocol, schemas, annotations, and
|
|
63
107
|
a scan-only pass over every description an agent would read before
|
|
@@ -113,7 +157,7 @@ Options
|
|
|
113
157
|
*/
|
|
114
158
|
const BOOLEAN_FLAGS = new Set([
|
|
115
159
|
'json', 'dry-run', 'all', 'estimate', 'help', 'version', 'allow-network',
|
|
116
|
-
'static-only', 'no-feed',
|
|
160
|
+
'static-only', 'no-feed', 'github', 'cron', 'weekly',
|
|
117
161
|
]);
|
|
118
162
|
|
|
119
163
|
function parseArgs(argv) {
|
|
@@ -137,12 +181,34 @@ const log = (m) => console.error(`${new Date().toISOString().slice(11, 19)} ${m}
|
|
|
137
181
|
async function main() {
|
|
138
182
|
const args = parseArgs(process.argv.slice(2));
|
|
139
183
|
if (args.version) { console.log(VERSION); return; }
|
|
140
|
-
if (args.help
|
|
184
|
+
if (args.help) { console.log(HELP); return; }
|
|
185
|
+
if (args._.length === 0) { console.log(QUICKSTART); return; }
|
|
141
186
|
|
|
142
|
-
|
|
187
|
+
const [cmd, link] = args._;
|
|
143
188
|
|
|
189
|
+
if (cmd === 'audit' || cmd === 'scan' || cmd === 'recommend') {
|
|
190
|
+
const target = args._[1] || process.cwd();
|
|
191
|
+
const r = await auditProject(target, {
|
|
192
|
+
out: typeof args.out === 'string' ? args.out : null,
|
|
193
|
+
api: typeof args.api === 'string' ? args.api : undefined,
|
|
194
|
+
historyDir: typeof args.history === 'string' ? args.history : undefined,
|
|
195
|
+
});
|
|
196
|
+
if (!r.ok) { console.error(`audit: ${r.why}`); process.exitCode = 1; return; }
|
|
197
|
+
if (args.json) { console.log(JSON.stringify(r, null, 2)); return; }
|
|
198
|
+
console.log(renderAudit(r));
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
144
201
|
|
|
145
|
-
|
|
202
|
+
if (cmd === 'schedule') {
|
|
203
|
+
const target = args._[1] || process.cwd();
|
|
204
|
+
const r = await scheduleReport(target, {
|
|
205
|
+
github: Boolean(args.github),
|
|
206
|
+
});
|
|
207
|
+
if (!r.ok) { console.error(`schedule: ${r.why}`); process.exitCode = 1; return; }
|
|
208
|
+
if (args.json) { console.log(JSON.stringify(r, null, 2)); return; }
|
|
209
|
+
console.log(renderSchedule(r));
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
146
212
|
|
|
147
213
|
if (cmd === 'doctor') {
|
|
148
214
|
const d = await doctor(args._[1] || process.cwd());
|
|
@@ -248,6 +314,42 @@ async function main() {
|
|
|
248
314
|
return;
|
|
249
315
|
}
|
|
250
316
|
|
|
317
|
+
// The push half. Two verbs, and neither is meant to be typed by a human:
|
|
318
|
+
// the SessionStart hook calls `consume`, then `refresh` detached.
|
|
319
|
+
if (cmd === 'notify') {
|
|
320
|
+
const sub = args._[1];
|
|
321
|
+
const root = args.root || process.cwd();
|
|
322
|
+
|
|
323
|
+
if (sub === 'consume') {
|
|
324
|
+
const file = args.digest || join(root, DEFAULT_DIGEST);
|
|
325
|
+
const text = consumeDigest(file);
|
|
326
|
+
if (text) console.log(text);
|
|
327
|
+
// Nothing to say is exit 0 and silence, not an error. See rule 1 in
|
|
328
|
+
// notify.mjs: a notifier that speaks every session gets ignored.
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
if (sub === 'refresh') {
|
|
333
|
+
const api = (args.api || DEFAULT_API).replace(/\/+$/, '');
|
|
334
|
+
try {
|
|
335
|
+
const r = await refreshNotify({ root, api, limit: Number(args.limit) > 0 ? Number(args.limit) : 50 });
|
|
336
|
+
if (args.json) console.log(JSON.stringify(r, null, 2));
|
|
337
|
+
else if (r.firstRun) console.log('First run: cursor established, nothing announced.');
|
|
338
|
+
else console.log(r.wrote ? `digest written to ${r.digest}` : 'nothing new, no digest written');
|
|
339
|
+
} catch (e) {
|
|
340
|
+
// Could not measure is 3, same as watch. This runs detached from a
|
|
341
|
+
// hook, so the exit code lands in .doorman/notify.log and nowhere else.
|
|
342
|
+
console.error(`notify refresh: ${e.message}`);
|
|
343
|
+
process.exitCode = e.code === 3 ? 3 : 1;
|
|
344
|
+
}
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
console.error('notify needs a subcommand: consume or refresh');
|
|
349
|
+
process.exitCode = 2;
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
|
|
251
353
|
if (!['report', 'eval'].includes(cmd)) {
|
|
252
354
|
console.error(`unknown command "${cmd}". Try: doorman --help`);
|
|
253
355
|
process.exitCode = 2;
|
package/doorman/cli/install.mjs
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* And safely inspects/wires .claude/settings.json without destructive clobbering.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
import { existsSync, mkdirSync, copyFileSync, readFileSync, writeFileSync } from 'node:fs';
|
|
17
|
+
import { existsSync, mkdirSync, copyFileSync, readFileSync, writeFileSync, chmodSync } from 'node:fs';
|
|
18
18
|
import { resolve, join, dirname } from 'node:path';
|
|
19
19
|
import { fileURLToPath } from 'node:url';
|
|
20
20
|
|
|
@@ -58,6 +58,19 @@ export async function install(targetDir = process.cwd(), { dryRun = false } = {}
|
|
|
58
58
|
|
|
59
59
|
// 1. Hooks, Subagent, Command
|
|
60
60
|
copy('.claude/hooks/mcp-gate.sh', '.claude/hooks/mcp-gate.sh');
|
|
61
|
+
// copyFileSync preserves the SOURCE mode, and the source inside an installed
|
|
62
|
+
// npm package is 644: npm pack on Windows drops the executable bit from every
|
|
63
|
+
// file. Verified against the published 0.1.0 tarball, 2026-09-12. Without this
|
|
64
|
+
// the gate lands non-executable, fails to SPAWN rather than exiting 2, and the
|
|
65
|
+
// security control fails open. install.sh has carried the same chmod since it
|
|
66
|
+
// was written; this path never did.
|
|
67
|
+
if (!dryRun) {
|
|
68
|
+
try {
|
|
69
|
+
chmodSync(join(absTarget, '.claude/hooks/mcp-gate.sh'), 0o755);
|
|
70
|
+
} catch {
|
|
71
|
+
warnings.push('could not mark .claude/hooks/mcp-gate.sh executable; run: chmod +x .claude/hooks/mcp-gate.sh');
|
|
72
|
+
}
|
|
73
|
+
}
|
|
61
74
|
copy('agents/doorman.md', '.claude/agents/doorman.md');
|
|
62
75
|
copy('commands/vet.md', '.claude/commands/vet.md');
|
|
63
76
|
|
|
@@ -170,6 +183,20 @@ export function renderInstall(res) {
|
|
|
170
183
|
L.push('');
|
|
171
184
|
}
|
|
172
185
|
|
|
173
|
-
L.push('
|
|
186
|
+
L.push('โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ');
|
|
187
|
+
L.push(' INSTALLATION COMPLETE ยท GETTING STARTED GUIDANCE');
|
|
188
|
+
L.push('โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ');
|
|
189
|
+
L.push(' 1. Run unified health & tool check:');
|
|
190
|
+
L.push(' doorman audit');
|
|
191
|
+
L.push('');
|
|
192
|
+
L.push(' 2. In Claude Code, manage your gate interactively:');
|
|
193
|
+
L.push(' /doorman');
|
|
194
|
+
L.push('');
|
|
195
|
+
L.push(' 3. Audit any candidate tool before adoption:');
|
|
196
|
+
L.push(' /vet <mcp_server_url>');
|
|
197
|
+
L.push('');
|
|
198
|
+
L.push(' 4. Set up recurring automated audit digests:');
|
|
199
|
+
L.push(' doorman schedule');
|
|
200
|
+
L.push('โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ');
|
|
174
201
|
return L.join('\n');
|
|
175
202
|
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The push half of the subscription.
|
|
3
|
+
*
|
|
4
|
+
* WHAT "PUSH" HONESTLY MEANS HERE. Nobody can push to a laptop behind NAT that
|
|
5
|
+
* is asleep half the day, and a doorman that opened a socket to wait for us
|
|
6
|
+
* would be a worse product than one that does not. So the push is not a new
|
|
7
|
+
* transport: it is the POLL BECOMING INVISIBLE. A SessionStart hook reads a
|
|
8
|
+
* digest that is already on disk and costs nothing, then fires a detached
|
|
9
|
+
* refresh so the NEXT session is current. From where the operator sits they
|
|
10
|
+
* never ran a command and the doorman told them anyway, which is the whole
|
|
11
|
+
* claim, and it is true.
|
|
12
|
+
*
|
|
13
|
+
* THREE RULES, AND EACH ONE IS A NOTIFICATION PRODUCT FAILING IF BROKEN:
|
|
14
|
+
*
|
|
15
|
+
* 1. SILENT WHEN THERE IS NOTHING. No digest file is written when nothing is
|
|
16
|
+
* new. A hook that prints "nothing new" at every session start teaches the
|
|
17
|
+
* operator to skip past the one session where it matters.
|
|
18
|
+
*
|
|
19
|
+
* 2. THE FIRST RUN SAYS NOTHING. With no cursor the feed returns everything
|
|
20
|
+
* graded so far. Announcing 26 rows as "new" the first time a plugin loads
|
|
21
|
+
* is not news, it is a catalogue nobody asked for. The first refresh
|
|
22
|
+
* establishes the cursor and writes no digest.
|
|
23
|
+
*
|
|
24
|
+
* 3. CONSUMED ONCE. The hook prints the digest and deletes it. Re-showing the
|
|
25
|
+
* same three servers every morning is how a notification becomes furniture.
|
|
26
|
+
*
|
|
27
|
+
* The network never runs inside the hook. A SessionStart hook that waits on a
|
|
28
|
+
* fetch makes every session start as slow as the slowest network it has ever
|
|
29
|
+
* been on, and offline it makes them all fail.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
import { readFileSync, writeFileSync, unlinkSync, existsSync, mkdirSync } from 'node:fs';
|
|
33
|
+
import { join, dirname } from 'node:path';
|
|
34
|
+
import { watch, readState, writeState, DEFAULT_API, DEFAULT_STATE } from './watch.mjs';
|
|
35
|
+
|
|
36
|
+
export const DEFAULT_DIGEST = join('.doorman', 'notify.md');
|
|
37
|
+
|
|
38
|
+
/** How many rows the digest will name before it stops listing and counts. */
|
|
39
|
+
export const MAX_LISTED = 3;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The digest, or null for "say nothing".
|
|
43
|
+
*
|
|
44
|
+
* Only two verdicts are ever worth interrupting a session for. `unreviewed` is
|
|
45
|
+
* something you could adopt and have not looked at. `blocked` is something the
|
|
46
|
+
* registry already refuses, and seeing it in a fresh grade is a reason to check
|
|
47
|
+
* whether anyone tried. `already-installed` and `skipped` are not news: the
|
|
48
|
+
* first is a server you have, and the second you decided about.
|
|
49
|
+
*/
|
|
50
|
+
export function renderNotify(result, { firstRun = false } = {}) {
|
|
51
|
+
if (firstRun) return null;
|
|
52
|
+
|
|
53
|
+
const rows = result.candidates ?? [];
|
|
54
|
+
const fresh = rows.filter((c) => c.verdict === 'unreviewed' && !c.is_fixture && !c.self_graded);
|
|
55
|
+
const blocked = rows.filter((c) => c.verdict === 'blocked');
|
|
56
|
+
if (!fresh.length && !blocked.length) return null;
|
|
57
|
+
|
|
58
|
+
const out = [];
|
|
59
|
+
out.push('## doorman');
|
|
60
|
+
out.push('');
|
|
61
|
+
|
|
62
|
+
if (fresh.length) {
|
|
63
|
+
const shown = fresh.slice(0, MAX_LISTED);
|
|
64
|
+
const noun = fresh.length === 1 ? 'server' : 'servers';
|
|
65
|
+
out.push(`${fresh.length} newly graded ${noun} this build does not have:`);
|
|
66
|
+
for (const c of shown) {
|
|
67
|
+
// The grade travels with what produced it. A letter with no model behind
|
|
68
|
+
// it invites a comparison across models that the grade cannot support.
|
|
69
|
+
const model = c.model ? ` on ${c.model}` : ', model not recorded';
|
|
70
|
+
const score = c.score === null || c.score === undefined ? 'not scored' : `${c.score}/100`;
|
|
71
|
+
out.push(`- **${c.grade ?? '?'}** ${score}${model} ${c.server_url}`);
|
|
72
|
+
const layers = c.layers ?? {};
|
|
73
|
+
const missing = ['behavioral_pct', 'guidance_pct']
|
|
74
|
+
.filter((k) => layers[k] === null || layers[k] === undefined)
|
|
75
|
+
.map((k) => k.replace('_pct', ''));
|
|
76
|
+
if (missing.length) out.push(` not measured: ${missing.join(', ')}`);
|
|
77
|
+
}
|
|
78
|
+
if (fresh.length > shown.length) {
|
|
79
|
+
out.push(`- and ${fresh.length - shown.length} more`);
|
|
80
|
+
}
|
|
81
|
+
out.push('');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (blocked.length) {
|
|
85
|
+
out.push(`${blocked.length} on your denylist were re-graded. Run \`doorman watch\` for the detail.`);
|
|
86
|
+
out.push('');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
out.push('Run `doorman needs` to see which of these answer something this build ' +
|
|
90
|
+
'keeps asking for, or `doorman watch` for the full list. Nothing has been ' +
|
|
91
|
+
'installed and nothing was sent anywhere: the match runs on this machine.');
|
|
92
|
+
return out.join('\n');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Fetch, classify, and leave a digest for the next session to find.
|
|
97
|
+
*
|
|
98
|
+
* Advances the cursor whether or not a digest is written, because the cursor
|
|
99
|
+
* tracks what has been SEEN by the subscription rather than what was worth
|
|
100
|
+
* mentioning. Not advancing it on a quiet day would re-announce the same rows
|
|
101
|
+
* the moment something interesting finally landed.
|
|
102
|
+
*/
|
|
103
|
+
export async function refreshNotify({
|
|
104
|
+
root = process.cwd(), api = DEFAULT_API, limit = 50,
|
|
105
|
+
stateFile, digestFile, fetchImpl = fetch,
|
|
106
|
+
} = {}) {
|
|
107
|
+
const state = stateFile ?? join(root, DEFAULT_STATE);
|
|
108
|
+
const digest = digestFile ?? join(root, DEFAULT_DIGEST);
|
|
109
|
+
|
|
110
|
+
const prior = readState(state);
|
|
111
|
+
const firstRun = !prior.since;
|
|
112
|
+
|
|
113
|
+
const result = await watch({ root, api, since: prior.since, limit, fetchImpl });
|
|
114
|
+
const text = renderNotify(result, { firstRun });
|
|
115
|
+
|
|
116
|
+
if (text) {
|
|
117
|
+
mkdirSync(dirname(digest), { recursive: true });
|
|
118
|
+
writeFileSync(digest, text + '\n', 'utf8');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (result.next_since) {
|
|
122
|
+
writeState(state, { since: result.next_since, seen: (prior.seen ?? 0) + result.candidates.length });
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return { wrote: Boolean(text), firstRun, digest, candidates: result.candidates.length };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* What the hook prints. Reading is destructive on purpose: see rule 3.
|
|
130
|
+
*
|
|
131
|
+
* Any failure here returns nothing rather than throwing. This runs at session
|
|
132
|
+
* start, and a notification that can break a session start is worse than no
|
|
133
|
+
* notification.
|
|
134
|
+
*/
|
|
135
|
+
export function consumeDigest(file) {
|
|
136
|
+
try {
|
|
137
|
+
if (!file || typeof file !== 'string') return null;
|
|
138
|
+
if (!existsSync(file)) return null;
|
|
139
|
+
const text = readFileSync(file, 'utf8').trim();
|
|
140
|
+
unlinkSync(file);
|
|
141
|
+
return text || null;
|
|
142
|
+
} catch {
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `doorman schedule` โ Configure automated, recurring tool audits & recommendation reports.
|
|
3
|
+
*
|
|
4
|
+
* Supports three automation modes:
|
|
5
|
+
* 1. Built-in SessionStart Hook (runs silently in background on session open)
|
|
6
|
+
* 2. GitHub Actions Workflow (--github: creates .github/workflows/doorman-audit.yml)
|
|
7
|
+
* 3. Agent / Cron Scheduling (prints crontab entry & /schedule agent prompt)
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { resolve, join } from 'node:path';
|
|
11
|
+
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
12
|
+
|
|
13
|
+
export const GITHUB_WORKFLOW_YAML = `name: Doorman Tool Audit & Security Report
|
|
14
|
+
|
|
15
|
+
on:
|
|
16
|
+
schedule:
|
|
17
|
+
# Run every Monday at 09:00 UTC
|
|
18
|
+
- cron: '0 9 * * 1'
|
|
19
|
+
workflow_dispatch:
|
|
20
|
+
|
|
21
|
+
jobs:
|
|
22
|
+
audit:
|
|
23
|
+
name: Run Clembot Doorman Audit
|
|
24
|
+
runs-on: ubuntu-latest
|
|
25
|
+
steps:
|
|
26
|
+
- name: Checkout repository
|
|
27
|
+
uses: actions/checkout@v4
|
|
28
|
+
|
|
29
|
+
- name: Setup Node.js
|
|
30
|
+
uses: actions/setup-node@v4
|
|
31
|
+
with:
|
|
32
|
+
node-version: 20
|
|
33
|
+
|
|
34
|
+
- name: Install Clembot Doorman
|
|
35
|
+
run: npm i -g clembot-doorman
|
|
36
|
+
|
|
37
|
+
- name: Run Unified Audit
|
|
38
|
+
run: |
|
|
39
|
+
mkdir -p .doorman
|
|
40
|
+
doorman audit --out .doorman/audit-report.md
|
|
41
|
+
|
|
42
|
+
- name: Upload Audit Artifact
|
|
43
|
+
uses: actions/upload-artifact@v4
|
|
44
|
+
with:
|
|
45
|
+
name: doorman-audit-report
|
|
46
|
+
path: .doorman/audit-report.md
|
|
47
|
+
`;
|
|
48
|
+
|
|
49
|
+
export async function schedule(targetDir = process.cwd(), opts = {}) {
|
|
50
|
+
const absRoot = resolve(targetDir);
|
|
51
|
+
|
|
52
|
+
if (opts.github) {
|
|
53
|
+
const wfDir = join(absRoot, '.github', 'workflows');
|
|
54
|
+
mkdirSync(wfDir, { recursive: true });
|
|
55
|
+
const wfPath = join(wfDir, 'doorman-audit.yml');
|
|
56
|
+
writeFileSync(wfPath, GITHUB_WORKFLOW_YAML, 'utf8');
|
|
57
|
+
return {
|
|
58
|
+
ok: true,
|
|
59
|
+
mode: 'github',
|
|
60
|
+
path: wfPath,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Check hook status
|
|
65
|
+
const sessionHook = join(absRoot, '.claude', 'hooks', 'session-notify.sh');
|
|
66
|
+
const hasHook = existsSync(sessionHook);
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
ok: true,
|
|
70
|
+
mode: 'guide',
|
|
71
|
+
root: absRoot,
|
|
72
|
+
hasHook,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function renderSchedule(res) {
|
|
77
|
+
const lines = [];
|
|
78
|
+
lines.push('');
|
|
79
|
+
lines.push('================================================================');
|
|
80
|
+
lines.push(' CLEMBOT DOORMAN ยท AUTOMATED AUDIT SCHEDULING ');
|
|
81
|
+
lines.push('================================================================');
|
|
82
|
+
lines.push('');
|
|
83
|
+
|
|
84
|
+
if (res.mode === 'github') {
|
|
85
|
+
lines.push(` โ Created GitHub Actions workflow at:`);
|
|
86
|
+
lines.push(` ${res.path}`);
|
|
87
|
+
lines.push('');
|
|
88
|
+
lines.push(' This workflow runs `doorman audit` every Monday at 09:00 UTC');
|
|
89
|
+
lines.push(' and uploads the markdown report as an artifact.');
|
|
90
|
+
lines.push('');
|
|
91
|
+
return lines.join('\n');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
lines.push('Choose how you want to schedule your tool audit & recommendations:');
|
|
95
|
+
lines.push('');
|
|
96
|
+
|
|
97
|
+
// 1. SessionStart Hook
|
|
98
|
+
lines.push('โโ 1. BUILT-IN SESSION HOOK (ZERO CONFIG) โโโโโโโโโโโโโโโโโโโโโโโ');
|
|
99
|
+
if (res.hasHook) {
|
|
100
|
+
lines.push(' [ACTIVE] .claude/hooks/session-notify.sh is installed.');
|
|
101
|
+
lines.push(' Claude Code automatically checks for new graded MCP tools at the');
|
|
102
|
+
lines.push(' start of every session and notifies you if fresh tools are available.');
|
|
103
|
+
} else {
|
|
104
|
+
lines.push(' Install the plugin to enable silent background session checks:');
|
|
105
|
+
lines.push(' claude plugin marketplace add clemenswan/clembot-doorman && claude plugin install clembot-doorman');
|
|
106
|
+
}
|
|
107
|
+
lines.push('');
|
|
108
|
+
|
|
109
|
+
// 2. AI Agent Loop / Slash Command
|
|
110
|
+
lines.push('โโ 2. AI AGENT RECURRING SCHEDULING โโโโโโโโโโโโโโโโโโโโโโโโโโโโ');
|
|
111
|
+
lines.push(' In Antigravity or pair-programming chat, invoke the /schedule command:');
|
|
112
|
+
lines.push(' /schedule CronExpression="0 9 * * 1", Prompt="Run doorman audit and report recommended MCP tools"');
|
|
113
|
+
lines.push('');
|
|
114
|
+
|
|
115
|
+
// 3. GitHub Actions
|
|
116
|
+
lines.push('โโ 3. CI/CD GITHUB ACTIONS (RECOMMENDED FOR TEAMS) โโโโโโโโโโโโโโ');
|
|
117
|
+
lines.push(' Generate a weekly automated audit workflow with:');
|
|
118
|
+
lines.push(' doorman schedule --github');
|
|
119
|
+
lines.push('');
|
|
120
|
+
|
|
121
|
+
// 4. System Cron
|
|
122
|
+
lines.push('โโ 4. SYSTEM CRON / TASK SCHEDULER โโโโโโโโโโโโโโโโโโโโโโโโโโโโโ');
|
|
123
|
+
lines.push(' Add to your crontab (crontab -e) to generate weekly markdown reports:');
|
|
124
|
+
lines.push(` 0 9 * * 1 cd "${res.root}" && npx clembot-doorman audit --out .doorman/audit.md`);
|
|
125
|
+
lines.push('');
|
|
126
|
+
|
|
127
|
+
return lines.join('\n');
|
|
128
|
+
}
|
|
@@ -29,8 +29,7 @@ If it prints `NOT_FOUND`, stop and tell the user this, then end the turn:
|
|
|
29
29
|
> Reinstall the plugin, or install the CLI directly:
|
|
30
30
|
>
|
|
31
31
|
> ```bash
|
|
32
|
-
>
|
|
33
|
-
> npm i -g ./clembot-doorman
|
|
32
|
+
> npm i -g clembot-doorman
|
|
34
33
|
> ```
|
|
35
34
|
>
|
|
36
35
|
> The gate is still running either way. It is pure bash and depends on none of
|
package/doorman/hooks/hooks.json
CHANGED
|
@@ -11,7 +11,16 @@
|
|
|
11
11
|
"",
|
|
12
12
|
"Timeout matches .claude/settings.json. The gate does no network and no",
|
|
13
13
|
"subprocess, so 5s is generous; it exists so a wedged hook fails rather",
|
|
14
|
-
"than hanging every tool call."
|
|
14
|
+
"than hanging every tool call.",
|
|
15
|
+
"",
|
|
16
|
+
"BOTH COMMANDS RUN THROUGH `bash` ON PURPOSE. npm pack on Windows drops",
|
|
17
|
+
"the executable bit from every file, so the .sh files inside an installed",
|
|
18
|
+
"package are 644 and cannot be invoked by path. Verified against the",
|
|
19
|
+
"published 0.1.0 tarball on 2026-09-12: mcp-gate.sh ships -rw-r--r--.",
|
|
20
|
+
"A gate that cannot start does not exit 2, it fails to spawn, so the",
|
|
21
|
+
"security control FAILS OPEN, which is the one thing its design forbids.",
|
|
22
|
+
"Invoking through the interpreter makes the mode irrelevant on every",
|
|
23
|
+
"platform. Do not `simplify` this back to a bare path."
|
|
15
24
|
],
|
|
16
25
|
"hooks": {
|
|
17
26
|
"PreToolUse": [
|
|
@@ -20,11 +29,33 @@
|
|
|
20
29
|
"hooks": [
|
|
21
30
|
{
|
|
22
31
|
"type": "command",
|
|
23
|
-
"command": "${CLAUDE_PLUGIN_ROOT}/.claude/hooks/mcp-gate.sh",
|
|
32
|
+
"command": "bash ${CLAUDE_PLUGIN_ROOT}/.claude/hooks/mcp-gate.sh",
|
|
24
33
|
"timeout": 5
|
|
25
34
|
}
|
|
26
35
|
]
|
|
27
36
|
}
|
|
37
|
+
],
|
|
38
|
+
"SessionStart": [
|
|
39
|
+
{
|
|
40
|
+
"hooks": [
|
|
41
|
+
{
|
|
42
|
+
"type": "command",
|
|
43
|
+
"command": "bash ${CLAUDE_PLUGIN_ROOT}/.claude/hooks/session-notify.sh",
|
|
44
|
+
"timeout": 10
|
|
45
|
+
}
|
|
46
|
+
]
|
|
47
|
+
}
|
|
28
48
|
]
|
|
29
|
-
}
|
|
49
|
+
},
|
|
50
|
+
"$comment2": [
|
|
51
|
+
"SessionStart is the PUSH surface, and it is a different kind of hook from",
|
|
52
|
+
"the gate above. The gate is a security control: offline, dependency-free,",
|
|
53
|
+
"and it fails CLOSED with exit 2. The notifier fails OPEN and silent, always",
|
|
54
|
+
"exits 0, and prints nothing on every error path, because a notification",
|
|
55
|
+
"that can break a session start is worse than no notification.",
|
|
56
|
+
"",
|
|
57
|
+
"It makes no network call. It prints a digest already on disk and fires a",
|
|
58
|
+
"detached refresh for next time, so the cost on a normal session is one",
|
|
59
|
+
"file test. The 10s timeout is for a slow disk, not for a fetch."
|
|
60
|
+
]
|
|
30
61
|
}
|
package/doorman/package.json
CHANGED
|
@@ -32,5 +32,9 @@ Worth separating, because the project doc uses one word for both.
|
|
|
32
32
|
its own past is not a record. The account is still the blocker, not the spec.
|
|
33
33
|
Full detail in the root `README.md` under **Bazantic**.
|
|
34
34
|
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
The three canonical Bazantic platform prize recipes are published in
|
|
36
|
+
[`bazantic/recipes/`](../../bazantic/recipes/):
|
|
37
|
+
1. `01-vet-mcp-candidate.json` โ Agentify an API ($500/300/200)
|
|
38
|
+
2. `02-deepwiki-pinned-researcher.json` โ Help an Agent Use Your Project / Continuity (2ร$500)
|
|
39
|
+
3. `03-governance-pipeline.json` โ Best Multi-API Recipe ($500/300/200)
|
|
40
|
+
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "clembot-doorman",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "The Package Manager & Security Doorman for Claude Code: inspect your build, recommend vetted MCPs from prompt history, and block rogue tools before context.",
|
|
5
5
|
"homepage": "https://clembot-doorman.wanessalabs.com",
|
|
6
6
|
"type": "module",
|