brainclaw 0.28.0 → 1.5.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +193 -170
- package/dist/brainclaw-vscode.vsix +0 -0
- package/dist/cli.js +683 -23
- package/dist/commands/accept.js +3 -0
- package/dist/commands/add-step.js +11 -26
- package/dist/commands/agent-board.js +70 -3
- package/dist/commands/audit.js +19 -0
- package/dist/commands/check-policy.js +54 -0
- package/dist/commands/check-security-mcp.js +145 -0
- package/dist/commands/check-security.js +106 -0
- package/dist/commands/claim-resource.js +1 -0
- package/dist/commands/codev.js +672 -0
- package/dist/commands/compact.js +74 -0
- package/dist/commands/complete-step.js +16 -26
- package/dist/commands/constraint.js +8 -20
- package/dist/commands/decision.js +9 -20
- package/dist/commands/delete-plan.js +10 -12
- package/dist/commands/delete-step.js +16 -0
- package/dist/commands/dispatch.js +163 -0
- package/dist/commands/doctor.js +1122 -49
- package/dist/commands/enable-agent.js +1 -0
- package/dist/commands/export.js +280 -22
- package/dist/commands/handoff.js +33 -0
- package/dist/commands/harvest.js +189 -0
- package/dist/commands/hooks.js +82 -25
- package/dist/commands/inbox.js +169 -0
- package/dist/commands/init.js +38 -31
- package/dist/commands/install-hooks.js +71 -44
- package/dist/commands/link.js +89 -0
- package/dist/commands/list-claims.js +48 -3
- package/dist/commands/list-plans.js +129 -25
- package/dist/commands/loops-handlers.js +409 -0
- package/dist/commands/mcp-read-handlers.js +1628 -0
- package/dist/commands/mcp-schemas.generated.js +74 -0
- package/dist/commands/mcp.js +4244 -1475
- package/dist/commands/plan-resource.js +64 -0
- package/dist/commands/plan.js +12 -26
- package/dist/commands/prune.js +37 -2
- package/dist/commands/reflect.js +20 -7
- package/dist/commands/release-claim.js +11 -6
- package/dist/commands/release-notes.js +170 -0
- package/dist/commands/repair.js +210 -0
- package/dist/commands/run-profile.js +57 -0
- package/dist/commands/sequence.js +113 -0
- package/dist/commands/session-end.js +423 -14
- package/dist/commands/session-start.js +214 -41
- package/dist/commands/setup-security.js +103 -0
- package/dist/commands/setup.js +42 -4
- package/dist/commands/stale.js +109 -0
- package/dist/commands/switch.js +131 -10
- package/dist/commands/trap.js +14 -31
- package/dist/commands/update-handoff.js +63 -4
- package/dist/commands/update-plan.js +21 -28
- package/dist/commands/update-step.js +37 -0
- package/dist/commands/upgrade.js +313 -6
- package/dist/commands/usage.js +102 -0
- package/dist/commands/version.js +20 -0
- package/dist/commands/who.js +124 -0
- package/dist/commands/worktree.js +105 -0
- package/dist/core/actions.js +315 -0
- package/dist/core/agent-capability.js +610 -17
- package/dist/core/agent-context.js +7 -1
- package/dist/core/agent-files.js +1169 -85
- package/dist/core/agent-integrations.js +160 -5
- package/dist/core/agent-inventory.js +2 -0
- package/dist/core/agent-profiles.js +93 -0
- package/dist/core/agent-registry.js +162 -30
- package/dist/core/agentrun-reconciler.js +345 -0
- package/dist/core/agentruns.js +424 -0
- package/dist/core/ai-agent-detection.js +31 -10
- package/dist/core/archival.js +77 -0
- package/dist/core/assignment-sweeper.js +82 -0
- package/dist/core/assignments.js +367 -0
- package/dist/core/audit.js +30 -0
- package/dist/core/bootstrap.js +61 -10
- package/dist/core/brainclaw-version.js +94 -2
- package/dist/core/candidates.js +93 -2
- package/dist/core/claims.js +419 -0
- package/dist/core/codev-metrics.js +77 -0
- package/dist/core/codev-personas.js +31 -0
- package/dist/core/codev-plan-gen.js +35 -0
- package/dist/core/codev-prompts.js +74 -0
- package/dist/core/codev-responses.js +62 -0
- package/dist/core/codev-rounds.js +218 -0
- package/dist/core/config.js +4 -0
- package/dist/core/context.js +454 -34
- package/dist/core/coordination.js +201 -6
- package/dist/core/cross-project.js +230 -16
- package/dist/core/default-profiles/doctor.yaml +11 -0
- package/dist/core/default-profiles/janitor.yaml +11 -0
- package/dist/core/default-profiles/onboarder.yaml +11 -0
- package/dist/core/default-profiles/reviewer.yaml +13 -0
- package/dist/core/dispatcher.js +1189 -0
- package/dist/core/duplicates.js +2 -2
- package/dist/core/entity-operations.js +450 -0
- package/dist/core/entity-registry.js +344 -0
- package/dist/core/event-log.js +1 -0
- package/dist/core/events.js +106 -2
- package/dist/core/execution-adapters.js +154 -0
- package/dist/core/execution-context.js +63 -0
- package/dist/core/execution-profile.js +270 -0
- package/dist/core/execution.js +255 -0
- package/dist/core/facade-schema.js +81 -0
- package/dist/core/federation-cloud.js +99 -0
- package/dist/core/federation-message.js +52 -0
- package/dist/core/federation-transport.js +65 -0
- package/dist/core/gc-semantic.js +482 -0
- package/dist/core/governance.js +247 -0
- package/dist/core/guards.js +19 -0
- package/dist/core/ideation.js +72 -0
- package/dist/core/identity.js +252 -28
- package/dist/core/ids.js +6 -0
- package/dist/core/input-validation.js +2 -2
- package/dist/core/instruction-templates.js +344 -136
- package/dist/core/io.js +90 -11
- package/dist/core/lock.js +6 -2
- package/dist/core/loops/brief-assembly.js +213 -0
- package/dist/core/loops/facade-schema.js +148 -0
- package/dist/core/loops/index.js +7 -0
- package/dist/core/loops/iteration-engine.js +139 -0
- package/dist/core/loops/lock.js +385 -0
- package/dist/core/loops/store.js +201 -0
- package/dist/core/loops/types.js +403 -0
- package/dist/core/loops/verbs.js +534 -0
- package/dist/core/markdown.js +15 -3
- package/dist/core/memory-compactor.js +432 -0
- package/dist/core/memory-git.js +152 -8
- package/dist/core/messaging.js +278 -0
- package/dist/core/migration.js +32 -1
- package/dist/core/mutation-pipeline.js +4 -2
- package/dist/core/operations/memory-mutation.js +129 -0
- package/dist/core/operations/memory-write.js +78 -0
- package/dist/core/operations/plan.js +190 -0
- package/dist/core/policy.js +169 -0
- package/dist/core/repo-analysis.js +67 -0
- package/dist/core/reputation.js +9 -3
- package/dist/core/schema.js +546 -21
- package/dist/core/search.js +21 -2
- package/dist/core/security-cache.js +71 -0
- package/dist/core/security-guard.js +152 -0
- package/dist/core/security-scoring.js +86 -0
- package/dist/core/sequence.js +130 -0
- package/dist/core/socket-client.js +113 -0
- package/dist/core/staleness.js +246 -0
- package/dist/core/state.js +98 -22
- package/dist/core/store-resolution.js +54 -12
- package/dist/core/toml-writer.js +76 -0
- package/dist/core/upgrades/backup.js +232 -0
- package/dist/core/upgrades/health-check.js +169 -0
- package/dist/core/upgrades/patches/candidate-archive.js +145 -0
- package/dist/core/upgrades/patches/handoff-review-strip.js +128 -0
- package/dist/core/upgrades/patches/provenance-rollout.js +136 -0
- package/dist/core/upgrades/schema-version.js +97 -0
- package/dist/core/worktree.js +606 -0
- package/dist/facts.js +114 -0
- package/dist/facts.json +111 -0
- package/docs/architecture/project-refs.md +5 -1
- package/docs/cli.md +690 -43
- package/docs/concepts/ideation-loop.md +317 -0
- package/docs/concepts/loop-engine.md +456 -0
- package/docs/concepts/mcp-governance.md +268 -0
- package/docs/concepts/memory-staleness.md +122 -0
- package/docs/concepts/multi-agent-workflows.md +166 -0
- package/docs/concepts/plans-and-claims.md +31 -6
- package/docs/concepts/project-md-convention.md +35 -0
- package/docs/concepts/troubleshooting.md +220 -0
- package/docs/concepts/upgrade-cli.md +202 -0
- package/docs/concepts/upgrade-dogfood-procedure.md +114 -0
- package/docs/context-format-changelog.md +2 -2
- package/docs/context-format.md +2 -2
- package/docs/index.md +68 -0
- package/docs/integrations/agents.md +15 -16
- package/docs/integrations/cline.md +88 -0
- package/docs/integrations/codex.md +75 -23
- package/docs/integrations/continue.md +60 -0
- package/docs/integrations/copilot.md +67 -9
- package/docs/integrations/kilocode.md +72 -0
- package/docs/integrations/mcp.md +304 -21
- package/docs/integrations/mistral-vibe.md +122 -0
- package/docs/integrations/opencode.md +84 -0
- package/docs/integrations/overview.md +23 -8
- package/docs/integrations/roo.md +74 -0
- package/docs/integrations/windsurf.md +83 -0
- package/docs/mcp-schema-changelog.md +191 -1
- package/docs/playbooks/integration/index.md +121 -0
- package/docs/playbooks/productivity/index.md +102 -0
- package/docs/playbooks/team/index.md +122 -0
- package/docs/product/agent-first-model.md +184 -0
- package/docs/product/entity-model-audit.md +462 -0
- package/docs/quickstart-existing-project.md +135 -0
- package/docs/quickstart.md +124 -37
- package/docs/release-maintenance.md +79 -0
- package/docs/review.md +2 -0
- package/docs/server-operations.md +118 -0
- package/package.json +20 -12
- package/dist/commands/claude-desktop-extension.js +0 -18
- package/dist/commands/diff.js +0 -99
- package/dist/core/claude-desktop-extension.js +0 -224
package/dist/core/search.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { loadState } from './state.js';
|
|
2
2
|
import { listCandidates } from './candidates.js';
|
|
3
|
+
import { listSequences } from './sequence.js';
|
|
3
4
|
const K1 = 1.5;
|
|
4
5
|
const B = 0.75;
|
|
5
6
|
function tokenize(text) {
|
|
@@ -9,7 +10,7 @@ function tokenize(text) {
|
|
|
9
10
|
.split(/\s+/)
|
|
10
11
|
.filter(t => t.length > 1);
|
|
11
12
|
}
|
|
12
|
-
function buildCorpus(state, includePending) {
|
|
13
|
+
function buildCorpus(state, includePending, cwd) {
|
|
13
14
|
const docs = [];
|
|
14
15
|
const add = (section, item) => {
|
|
15
16
|
const textParts = [item.text, item.author ?? '', ...(item.tags ?? []), ...(item.related_paths ?? [])];
|
|
@@ -25,6 +26,24 @@ function buildCorpus(state, includePending) {
|
|
|
25
26
|
add('handoffs', { ...h, text: `${h.from} -> ${h.to}: ${h.text}` });
|
|
26
27
|
for (const p of state.plan_items)
|
|
27
28
|
add('plans', p);
|
|
29
|
+
// Index sequences: combine name, description, item labels/lanes into searchable text
|
|
30
|
+
try {
|
|
31
|
+
const sequences = listSequences(cwd);
|
|
32
|
+
for (const seq of sequences) {
|
|
33
|
+
const itemTexts = (seq.items ?? []).map((it) => [it.lane, it.scope_hint, it.label, it.rationale].filter(Boolean).join(' '));
|
|
34
|
+
const text = [seq.name, seq.description ?? '', ...itemTexts].join(' — ');
|
|
35
|
+
add('sequences', {
|
|
36
|
+
id: seq.id,
|
|
37
|
+
text,
|
|
38
|
+
author: seq.author,
|
|
39
|
+
created_at: seq.created_at,
|
|
40
|
+
tags: seq.tags ?? [],
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
// Sequences dir may not exist yet — non-fatal
|
|
46
|
+
}
|
|
28
47
|
if (includePending) {
|
|
29
48
|
const candidates = listCandidates('pending');
|
|
30
49
|
for (const c of candidates)
|
|
@@ -109,7 +128,7 @@ export function searchCorpus(documents, options) {
|
|
|
109
128
|
}
|
|
110
129
|
export function search(options) {
|
|
111
130
|
const state = loadState(options.cwd);
|
|
112
|
-
const corpus = buildCorpus(state, options.includePending ?? false);
|
|
131
|
+
const corpus = buildCorpus(state, options.includePending ?? false, options.cwd);
|
|
113
132
|
return searchCorpus(corpus, options);
|
|
114
133
|
}
|
|
115
134
|
//# sourceMappingURL=search.js.map
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
function cacheKey(ecosystem, depname, version) {
|
|
4
|
+
return `${ecosystem}/${depname}@${version}`;
|
|
5
|
+
}
|
|
6
|
+
function loadStore(cachePath) {
|
|
7
|
+
try {
|
|
8
|
+
const raw = fs.readFileSync(cachePath, 'utf-8');
|
|
9
|
+
const parsed = JSON.parse(raw);
|
|
10
|
+
if (parsed?.version === 1 && typeof parsed.entries === 'object') {
|
|
11
|
+
return parsed;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
// file missing or corrupt — start fresh
|
|
16
|
+
}
|
|
17
|
+
return { version: 1, entries: {} };
|
|
18
|
+
}
|
|
19
|
+
function saveStore(cachePath, store) {
|
|
20
|
+
const dir = path.dirname(cachePath);
|
|
21
|
+
if (!fs.existsSync(dir))
|
|
22
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
23
|
+
fs.writeFileSync(cachePath, JSON.stringify(store, null, 2), 'utf-8');
|
|
24
|
+
}
|
|
25
|
+
export class SecurityCache {
|
|
26
|
+
store;
|
|
27
|
+
cachePath;
|
|
28
|
+
ttlMs;
|
|
29
|
+
constructor(cachePath, ttlHours = 24) {
|
|
30
|
+
this.cachePath = cachePath;
|
|
31
|
+
this.ttlMs = ttlHours * 60 * 60 * 1000;
|
|
32
|
+
this.store = loadStore(cachePath);
|
|
33
|
+
}
|
|
34
|
+
get(ecosystem, depname, version) {
|
|
35
|
+
const key = cacheKey(ecosystem, depname, version);
|
|
36
|
+
const entry = this.store.entries[key];
|
|
37
|
+
if (!entry)
|
|
38
|
+
return null;
|
|
39
|
+
const age = Date.now() - new Date(entry.fetched_at).getTime();
|
|
40
|
+
if (age > this.ttlMs) {
|
|
41
|
+
delete this.store.entries[key];
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
return entry.scores;
|
|
45
|
+
}
|
|
46
|
+
set(ecosystem, depname, version, scores) {
|
|
47
|
+
const key = cacheKey(ecosystem, depname, version);
|
|
48
|
+
this.store.entries[key] = {
|
|
49
|
+
scores,
|
|
50
|
+
fetched_at: new Date().toISOString(),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
flush() {
|
|
54
|
+
saveStore(this.cachePath, this.store);
|
|
55
|
+
}
|
|
56
|
+
prune() {
|
|
57
|
+
const now = Date.now();
|
|
58
|
+
let pruned = 0;
|
|
59
|
+
for (const [key, entry] of Object.entries(this.store.entries)) {
|
|
60
|
+
if (now - new Date(entry.fetched_at).getTime() > this.ttlMs) {
|
|
61
|
+
delete this.store.entries[key];
|
|
62
|
+
pruned++;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return pruned;
|
|
66
|
+
}
|
|
67
|
+
size() {
|
|
68
|
+
return Object.keys(this.store.entries).length;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
//# sourceMappingURL=security-cache.js.map
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generates brainclaw-guard wrapper scripts (bash + PowerShell) that intercept
|
|
3
|
+
* npm/pip install commands and check packages via brainclaw check-security.
|
|
4
|
+
*/
|
|
5
|
+
export function generateBashGuard(brainclawBin) {
|
|
6
|
+
return `#!/usr/bin/env bash
|
|
7
|
+
# brainclaw-guard — preinstall security gate
|
|
8
|
+
# Generated by: brainclaw setup --security
|
|
9
|
+
# Do not edit manually — regenerate with brainclaw setup --security
|
|
10
|
+
|
|
11
|
+
BRAINCLAW_BIN="${brainclawBin}"
|
|
12
|
+
ORIGINAL_CMD="\${BRAINCLAW_GUARD_ORIGINAL_CMD:-npm}"
|
|
13
|
+
|
|
14
|
+
is_install_command() {
|
|
15
|
+
for arg in "$@"; do
|
|
16
|
+
case "$arg" in
|
|
17
|
+
install|add|i) return 0 ;;
|
|
18
|
+
-*) continue ;;
|
|
19
|
+
*) break ;;
|
|
20
|
+
esac
|
|
21
|
+
done
|
|
22
|
+
return 1
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
extract_packages() {
|
|
26
|
+
local packages=""
|
|
27
|
+
local skip_next=false
|
|
28
|
+
local past_command=false
|
|
29
|
+
for arg in "$@"; do
|
|
30
|
+
if [ "$skip_next" = true ]; then
|
|
31
|
+
skip_next=false
|
|
32
|
+
continue
|
|
33
|
+
fi
|
|
34
|
+
case "$arg" in
|
|
35
|
+
install|add|i)
|
|
36
|
+
past_command=true
|
|
37
|
+
continue
|
|
38
|
+
;;
|
|
39
|
+
--save-dev|--save-peer|--save-optional|-D|-P|-O|-g|--global)
|
|
40
|
+
continue
|
|
41
|
+
;;
|
|
42
|
+
--registry|--prefix)
|
|
43
|
+
skip_next=true
|
|
44
|
+
continue
|
|
45
|
+
;;
|
|
46
|
+
-*)
|
|
47
|
+
continue
|
|
48
|
+
;;
|
|
49
|
+
*)
|
|
50
|
+
if [ "$past_command" = true ]; then
|
|
51
|
+
if [ -n "$packages" ]; then
|
|
52
|
+
packages="$packages,$arg"
|
|
53
|
+
else
|
|
54
|
+
packages="$arg"
|
|
55
|
+
fi
|
|
56
|
+
fi
|
|
57
|
+
;;
|
|
58
|
+
esac
|
|
59
|
+
done
|
|
60
|
+
echo "$packages"
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
# Main logic
|
|
64
|
+
if is_install_command "$@"; then
|
|
65
|
+
packages=$(extract_packages "$@")
|
|
66
|
+
if [ -n "$packages" ]; then
|
|
67
|
+
ecosystem="npm"
|
|
68
|
+
if [ "$ORIGINAL_CMD" = "pip" ] || [ "$ORIGINAL_CMD" = "pip3" ]; then
|
|
69
|
+
ecosystem="pypi"
|
|
70
|
+
fi
|
|
71
|
+
|
|
72
|
+
result=$("$BRAINCLAW_BIN" check-security --packages "$packages" --ecosystem "$ecosystem" --json 2>/dev/null)
|
|
73
|
+
exit_code=$?
|
|
74
|
+
|
|
75
|
+
if [ $exit_code -eq 2 ]; then
|
|
76
|
+
echo "[brainclaw-guard] BLOCKED — supply chain risk detected" >&2
|
|
77
|
+
echo "[brainclaw-guard] Run: brainclaw check-security --packages \\"$packages\\" for details" >&2
|
|
78
|
+
exit 1
|
|
79
|
+
elif [ $exit_code -eq 1 ]; then
|
|
80
|
+
echo "[brainclaw-guard] WARNING — potential supply chain risk" >&2
|
|
81
|
+
echo "[brainclaw-guard] Run: brainclaw check-security --packages \\"$packages\\" for details" >&2
|
|
82
|
+
fi
|
|
83
|
+
# exit_code 0 = pass, continue silently
|
|
84
|
+
fi
|
|
85
|
+
fi
|
|
86
|
+
|
|
87
|
+
exec "$ORIGINAL_CMD" "$@"
|
|
88
|
+
`;
|
|
89
|
+
}
|
|
90
|
+
export function generatePowerShellGuard(brainclawBin) {
|
|
91
|
+
return `# brainclaw-guard — preinstall security gate (PowerShell)
|
|
92
|
+
# Generated by: brainclaw setup --security
|
|
93
|
+
# Do not edit manually — regenerate with brainclaw setup --security
|
|
94
|
+
|
|
95
|
+
param([Parameter(ValueFromRemainingArguments)]$Args)
|
|
96
|
+
|
|
97
|
+
$BrainclawBin = "${brainclawBin}"
|
|
98
|
+
$OriginalCmd = if ($env:BRAINCLAW_GUARD_ORIGINAL_CMD) { $env:BRAINCLAW_GUARD_ORIGINAL_CMD } else { "npm" }
|
|
99
|
+
|
|
100
|
+
function Is-InstallCommand($arguments) {
|
|
101
|
+
foreach ($arg in $arguments) {
|
|
102
|
+
if ($arg -match "^(install|add|i)$") { return $true }
|
|
103
|
+
if ($arg -notmatch "^-") { break }
|
|
104
|
+
}
|
|
105
|
+
return $false
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function Extract-Packages($arguments) {
|
|
109
|
+
$packages = @()
|
|
110
|
+
$pastCommand = $false
|
|
111
|
+
$skipNext = $false
|
|
112
|
+
foreach ($arg in $arguments) {
|
|
113
|
+
if ($skipNext) { $skipNext = $false; continue }
|
|
114
|
+
if ($arg -match "^(install|add|i)$") { $pastCommand = $true; continue }
|
|
115
|
+
if ($arg -match "^--(registry|prefix)$") { $skipNext = $true; continue }
|
|
116
|
+
if ($arg -match "^-") { continue }
|
|
117
|
+
if ($pastCommand) { $packages += $arg }
|
|
118
|
+
}
|
|
119
|
+
return ($packages -join ",")
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (Is-InstallCommand $Args) {
|
|
123
|
+
$packages = Extract-Packages $Args
|
|
124
|
+
if ($packages) {
|
|
125
|
+
$ecosystem = "npm"
|
|
126
|
+
if ($OriginalCmd -match "^pip") { $ecosystem = "pypi" }
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
& $BrainclawBin check-security --packages $packages --ecosystem $ecosystem --json 2>$null | Out-Null
|
|
130
|
+
$exitCode = $LASTEXITCODE
|
|
131
|
+
} catch {
|
|
132
|
+
$exitCode = 0
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if ($exitCode -eq 2) {
|
|
136
|
+
Write-Error "[brainclaw-guard] BLOCKED - supply chain risk detected"
|
|
137
|
+
Write-Error "[brainclaw-guard] Run: brainclaw check-security --packages \`"$packages\`" for details"
|
|
138
|
+
exit 1
|
|
139
|
+
} elseif ($exitCode -eq 1) {
|
|
140
|
+
Write-Warning "[brainclaw-guard] WARNING - potential supply chain risk"
|
|
141
|
+
Write-Warning "[brainclaw-guard] Run: brainclaw check-security --packages \`"$packages\`" for details"
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
& $OriginalCmd @Args
|
|
147
|
+
`;
|
|
148
|
+
}
|
|
149
|
+
export function generatePipBashGuard(brainclawBin) {
|
|
150
|
+
return generateBashGuard(brainclawBin).replace('ORIGINAL_CMD="${BRAINCLAW_GUARD_ORIGINAL_CMD:-npm}"', 'ORIGINAL_CMD="${BRAINCLAW_GUARD_ORIGINAL_CMD:-pip}"');
|
|
151
|
+
}
|
|
152
|
+
//# sourceMappingURL=security-guard.js.map
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
const DEFAULT_WEIGHTS = {
|
|
2
|
+
supply_chain: 0.35,
|
|
3
|
+
vulnerability: 0.30,
|
|
4
|
+
quality: 0.15,
|
|
5
|
+
maintenance: 0.15,
|
|
6
|
+
license: 0.05,
|
|
7
|
+
};
|
|
8
|
+
const DEFAULT_THRESHOLDS = {
|
|
9
|
+
composite_pass: 70,
|
|
10
|
+
composite_warn: 50,
|
|
11
|
+
supply_chain_block: 30,
|
|
12
|
+
vulnerability_block: 20,
|
|
13
|
+
};
|
|
14
|
+
export function computeComposite(scores, weights) {
|
|
15
|
+
const w = { ...DEFAULT_WEIGHTS, ...weights };
|
|
16
|
+
return Math.round((scores.supplyChain * w.supply_chain +
|
|
17
|
+
scores.vulnerability * w.vulnerability +
|
|
18
|
+
scores.quality * w.quality +
|
|
19
|
+
scores.maintenance * w.maintenance +
|
|
20
|
+
scores.license * w.license) * 10) / 10;
|
|
21
|
+
}
|
|
22
|
+
export function evaluatePackage(scores, config) {
|
|
23
|
+
const thresholds = { ...DEFAULT_THRESHOLDS, ...config?.thresholds };
|
|
24
|
+
const weights = { ...DEFAULT_WEIGHTS, ...config?.weights };
|
|
25
|
+
const allowlist = config?.allowlist ?? [];
|
|
26
|
+
const denylist = config?.denylist ?? [];
|
|
27
|
+
const pkgName = scores.purl.replace(/^pkg:\w+\//, '');
|
|
28
|
+
const ecosystem = scores.purl.startsWith('pkg:pypi') ? 'pypi' : 'npm';
|
|
29
|
+
const reasons = [];
|
|
30
|
+
// Denylist check (exact match on package name)
|
|
31
|
+
if (denylist.some(d => pkgName === d || scores.purl.includes(d))) {
|
|
32
|
+
return {
|
|
33
|
+
package: pkgName,
|
|
34
|
+
ecosystem,
|
|
35
|
+
version: scores.version,
|
|
36
|
+
scores,
|
|
37
|
+
composite: 0,
|
|
38
|
+
decision: 'block',
|
|
39
|
+
reasons: [`Package "${pkgName}" is on the denylist`],
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
// Allowlist check (skip scoring)
|
|
43
|
+
if (allowlist.some(a => pkgName === a || scores.purl.includes(a))) {
|
|
44
|
+
return {
|
|
45
|
+
package: pkgName,
|
|
46
|
+
ecosystem,
|
|
47
|
+
version: scores.version,
|
|
48
|
+
scores,
|
|
49
|
+
composite: 100,
|
|
50
|
+
decision: 'pass',
|
|
51
|
+
reasons: [`Package "${pkgName}" is on the allowlist`],
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
const composite = computeComposite(scores, weights);
|
|
55
|
+
// Hard blocks on individual scores
|
|
56
|
+
if (scores.supplyChain < thresholds.supply_chain_block) {
|
|
57
|
+
reasons.push(`supply_chain=${scores.supplyChain} < ${thresholds.supply_chain_block} (hard block)`);
|
|
58
|
+
}
|
|
59
|
+
if (scores.vulnerability < thresholds.vulnerability_block) {
|
|
60
|
+
reasons.push(`vulnerability=${scores.vulnerability} < ${thresholds.vulnerability_block} (hard block)`);
|
|
61
|
+
}
|
|
62
|
+
if (reasons.length > 0) {
|
|
63
|
+
return { package: pkgName, ecosystem, version: scores.version, scores, composite, decision: 'block', reasons };
|
|
64
|
+
}
|
|
65
|
+
// Composite-based decision
|
|
66
|
+
if (composite < thresholds.composite_warn) {
|
|
67
|
+
reasons.push(`composite=${composite} < ${thresholds.composite_warn} (block threshold)`);
|
|
68
|
+
return { package: pkgName, ecosystem, version: scores.version, scores, composite, decision: 'block', reasons };
|
|
69
|
+
}
|
|
70
|
+
if (composite < thresholds.composite_pass) {
|
|
71
|
+
reasons.push(`composite=${composite} < ${thresholds.composite_pass} (warn threshold)`);
|
|
72
|
+
return { package: pkgName, ecosystem, version: scores.version, scores, composite, decision: 'warn', reasons };
|
|
73
|
+
}
|
|
74
|
+
return { package: pkgName, ecosystem, version: scores.version, scores, composite, decision: 'pass', reasons: [] };
|
|
75
|
+
}
|
|
76
|
+
export function evaluateBatch(scoresList, config) {
|
|
77
|
+
return scoresList.map(s => evaluatePackage(s, config));
|
|
78
|
+
}
|
|
79
|
+
export function worstDecision(verdicts) {
|
|
80
|
+
if (verdicts.some(v => v.decision === 'block'))
|
|
81
|
+
return 'block';
|
|
82
|
+
if (verdicts.some(v => v.decision === 'warn'))
|
|
83
|
+
return 'warn';
|
|
84
|
+
return 'pass';
|
|
85
|
+
}
|
|
86
|
+
//# sourceMappingURL=security-scoring.js.map
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import { JsonStore } from './json-store.js';
|
|
3
|
+
import { mutate } from './mutation-pipeline.js';
|
|
4
|
+
import { generateIdWithLabel, nowISO } from './ids.js';
|
|
5
|
+
import { resolveEntityDir } from './io.js';
|
|
6
|
+
import { SequenceItemSchema, SequenceSchema } from './schema.js';
|
|
7
|
+
import { refreshLiveCompanions } from '../commands/export.js';
|
|
8
|
+
function sequencesDir(cwd, mode = 'read') {
|
|
9
|
+
return resolveEntityDir('sequences', cwd ?? process.cwd(), mode);
|
|
10
|
+
}
|
|
11
|
+
export function ensureSequencesDir(cwd) {
|
|
12
|
+
const dir = sequencesDir(cwd, 'write');
|
|
13
|
+
if (!fs.existsSync(dir)) {
|
|
14
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function sequenceStore(cwd, mode = 'read') {
|
|
18
|
+
return new JsonStore({
|
|
19
|
+
dirPath: sequencesDir(cwd, mode),
|
|
20
|
+
documentType: 'sequence',
|
|
21
|
+
getId: (sequence) => sequence.id,
|
|
22
|
+
sort: (a, b) => a.updated_at.localeCompare(b.updated_at),
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
function normalizeItems(items) {
|
|
26
|
+
return items
|
|
27
|
+
.map((item) => SequenceItemSchema.parse(item))
|
|
28
|
+
.sort((a, b) => a.rank - b.rank || a.planId.localeCompare(b.planId));
|
|
29
|
+
}
|
|
30
|
+
function validateRanks(items) {
|
|
31
|
+
const ranks = new Set();
|
|
32
|
+
for (const item of items) {
|
|
33
|
+
if (ranks.has(item.rank)) {
|
|
34
|
+
throw new Error(`Duplicate sequence rank: ${item.rank}`);
|
|
35
|
+
}
|
|
36
|
+
ranks.add(item.rank);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
export function saveSequence(sequence, cwd) {
|
|
40
|
+
mutate({ cwd }, () => {
|
|
41
|
+
ensureSequencesDir(cwd);
|
|
42
|
+
sequenceStore(cwd, 'write').save(SequenceSchema.parse(sequence));
|
|
43
|
+
// Auto-refresh live companions after sequence changes (non-fatal)
|
|
44
|
+
try {
|
|
45
|
+
refreshLiveCompanions(cwd);
|
|
46
|
+
}
|
|
47
|
+
catch { /* best-effort */ }
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
export function listSequences(cwd) {
|
|
51
|
+
return sequenceStore(cwd).list().sort((a, b) => {
|
|
52
|
+
const activeBoost = Number(b.status === 'active') - Number(a.status === 'active');
|
|
53
|
+
if (activeBoost !== 0)
|
|
54
|
+
return activeBoost;
|
|
55
|
+
return b.updated_at.localeCompare(a.updated_at);
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
export function loadSequence(id, cwd) {
|
|
59
|
+
const sequence = listSequences(cwd).find((entry) => entry.id === id || entry.short_label === id);
|
|
60
|
+
if (!sequence) {
|
|
61
|
+
throw new Error(`Sequence not found: ${id}`);
|
|
62
|
+
}
|
|
63
|
+
return sequence;
|
|
64
|
+
}
|
|
65
|
+
export function getActiveSequence(cwd) {
|
|
66
|
+
return listSequences(cwd).find((sequence) => sequence.status === 'active');
|
|
67
|
+
}
|
|
68
|
+
export function createSequence(input, cwd) {
|
|
69
|
+
const items = normalizeItems(input.items ?? []);
|
|
70
|
+
validateRanks(items);
|
|
71
|
+
const { id, short_label } = generateIdWithLabel('sequences', cwd);
|
|
72
|
+
const timestamp = nowISO();
|
|
73
|
+
const sequence = {
|
|
74
|
+
id,
|
|
75
|
+
short_label,
|
|
76
|
+
name: input.name,
|
|
77
|
+
description: input.description,
|
|
78
|
+
status: input.status ?? 'draft',
|
|
79
|
+
items,
|
|
80
|
+
owner: input.owner,
|
|
81
|
+
created_at: timestamp,
|
|
82
|
+
updated_at: timestamp,
|
|
83
|
+
author: input.author,
|
|
84
|
+
author_id: input.authorId,
|
|
85
|
+
model: input.model,
|
|
86
|
+
project_id: input.projectId,
|
|
87
|
+
host_id: input.hostId,
|
|
88
|
+
session_id: input.sessionId,
|
|
89
|
+
tags: input.tags ?? [],
|
|
90
|
+
};
|
|
91
|
+
saveSequence(sequence, cwd);
|
|
92
|
+
return { id, shortLabel: short_label, name: input.name };
|
|
93
|
+
}
|
|
94
|
+
export function deleteSequence(id, cwd) {
|
|
95
|
+
return mutate({ cwd }, () => {
|
|
96
|
+
ensureSequencesDir(cwd);
|
|
97
|
+
const store = sequenceStore(cwd, 'write');
|
|
98
|
+
const current = store.list().find((entry) => entry.id === id || entry.short_label === id);
|
|
99
|
+
if (!current) {
|
|
100
|
+
throw new Error(`Sequence not found: ${id}`);
|
|
101
|
+
}
|
|
102
|
+
store.delete(current.id);
|
|
103
|
+
return { id: current.id, name: current.name };
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
export function updateSequence(input, cwd) {
|
|
107
|
+
return mutate({ cwd }, () => {
|
|
108
|
+
ensureSequencesDir(cwd);
|
|
109
|
+
const store = sequenceStore(cwd, 'write');
|
|
110
|
+
const current = store.list().find((entry) => entry.id === input.id || entry.short_label === input.id);
|
|
111
|
+
if (!current) {
|
|
112
|
+
throw new Error(`Sequence not found: ${input.id}`);
|
|
113
|
+
}
|
|
114
|
+
const items = input.items ? normalizeItems(input.items) : current.items;
|
|
115
|
+
validateRanks(items);
|
|
116
|
+
const next = {
|
|
117
|
+
...current,
|
|
118
|
+
name: input.name ?? current.name,
|
|
119
|
+
description: input.description ?? current.description,
|
|
120
|
+
status: input.status ?? current.status,
|
|
121
|
+
items,
|
|
122
|
+
owner: input.owner ?? current.owner,
|
|
123
|
+
tags: input.tags ?? current.tags,
|
|
124
|
+
updated_at: nowISO(),
|
|
125
|
+
};
|
|
126
|
+
store.save(SequenceSchema.parse(next));
|
|
127
|
+
return next;
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
//# sourceMappingURL=sequence.js.map
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import https from 'node:https';
|
|
2
|
+
const DEFAULT_ENDPOINT = 'https://mcp.socket.dev/';
|
|
3
|
+
const DEFAULT_TIMEOUT = 15_000;
|
|
4
|
+
const MCP_PROTOCOL_VERSION = '2024-11-05';
|
|
5
|
+
function post(url, body, headers, timeoutMs) {
|
|
6
|
+
return new Promise((resolve, reject) => {
|
|
7
|
+
const parsed = new URL(url);
|
|
8
|
+
const req = https.request({
|
|
9
|
+
hostname: parsed.hostname,
|
|
10
|
+
port: parsed.port || 443,
|
|
11
|
+
path: parsed.pathname,
|
|
12
|
+
method: 'POST',
|
|
13
|
+
headers: { 'Content-Type': 'application/json', 'Content-Length': String(Buffer.byteLength(body)), ...headers },
|
|
14
|
+
timeout: timeoutMs,
|
|
15
|
+
}, res => {
|
|
16
|
+
let data = '';
|
|
17
|
+
res.on('data', (c) => data += c.toString());
|
|
18
|
+
res.on('end', () => {
|
|
19
|
+
const h = {};
|
|
20
|
+
for (const [k, v] of Object.entries(res.headers)) {
|
|
21
|
+
if (typeof v === 'string')
|
|
22
|
+
h[k] = v;
|
|
23
|
+
}
|
|
24
|
+
resolve({ status: res.statusCode ?? 0, headers: h, body: data });
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
req.on('timeout', () => { req.destroy(); reject(new Error('Socket MCP request timed out')); });
|
|
28
|
+
req.on('error', reject);
|
|
29
|
+
req.write(body);
|
|
30
|
+
req.end();
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
function parseScoresFromText(text) {
|
|
34
|
+
const results = [];
|
|
35
|
+
for (const line of text.split('\n')) {
|
|
36
|
+
if (!line.startsWith('pkg:'))
|
|
37
|
+
continue;
|
|
38
|
+
const match = line.match(/^(pkg:\w+\/[^@]+)@([^:]+):\s*(.+)/);
|
|
39
|
+
if (!match)
|
|
40
|
+
continue;
|
|
41
|
+
const [, purl, version, rest] = match;
|
|
42
|
+
const scores = {};
|
|
43
|
+
for (const pair of rest.split(',')) {
|
|
44
|
+
const [k, v] = pair.trim().split(':').map(s => s.trim());
|
|
45
|
+
if (k && v)
|
|
46
|
+
scores[k] = parseInt(v, 10);
|
|
47
|
+
}
|
|
48
|
+
results.push({
|
|
49
|
+
purl: purl,
|
|
50
|
+
version: version,
|
|
51
|
+
supplyChain: scores['supplyChain'] ?? 0,
|
|
52
|
+
vulnerability: scores['vulnerability'] ?? 0,
|
|
53
|
+
quality: scores['quality'] ?? 0,
|
|
54
|
+
maintenance: scores['maintenance'] ?? 0,
|
|
55
|
+
license: scores['license'] ?? 0,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
return results;
|
|
59
|
+
}
|
|
60
|
+
export async function querySocketScores(packages, options = {}) {
|
|
61
|
+
const endpoint = options.endpoint ?? DEFAULT_ENDPOINT;
|
|
62
|
+
const timeout = options.timeoutMs ?? DEFAULT_TIMEOUT;
|
|
63
|
+
// Step 1: MCP initialize
|
|
64
|
+
const initBody = JSON.stringify({
|
|
65
|
+
jsonrpc: '2.0',
|
|
66
|
+
method: 'initialize',
|
|
67
|
+
params: {
|
|
68
|
+
protocolVersion: MCP_PROTOCOL_VERSION,
|
|
69
|
+
capabilities: {},
|
|
70
|
+
clientInfo: { name: 'brainclaw-security', version: '1.0.0' },
|
|
71
|
+
},
|
|
72
|
+
id: 1,
|
|
73
|
+
});
|
|
74
|
+
const initRes = await post(endpoint, initBody, {}, timeout);
|
|
75
|
+
if (initRes.status !== 200) {
|
|
76
|
+
throw new Error(`Socket MCP initialize failed: HTTP ${initRes.status}`);
|
|
77
|
+
}
|
|
78
|
+
const sessionId = initRes.headers['mcp-session-id'];
|
|
79
|
+
if (!sessionId) {
|
|
80
|
+
throw new Error('Socket MCP did not return a session ID');
|
|
81
|
+
}
|
|
82
|
+
// Step 2: Send initialized notification
|
|
83
|
+
const notifBody = JSON.stringify({ jsonrpc: '2.0', method: 'notifications/initialized' });
|
|
84
|
+
await post(endpoint, notifBody, { 'mcp-session-id': sessionId }, timeout);
|
|
85
|
+
// Step 3: Call depscore
|
|
86
|
+
const callBody = JSON.stringify({
|
|
87
|
+
jsonrpc: '2.0',
|
|
88
|
+
method: 'tools/call',
|
|
89
|
+
params: {
|
|
90
|
+
name: 'depscore',
|
|
91
|
+
arguments: { packages },
|
|
92
|
+
},
|
|
93
|
+
id: 2,
|
|
94
|
+
});
|
|
95
|
+
const callRes = await post(endpoint, callBody, { 'mcp-session-id': sessionId }, timeout);
|
|
96
|
+
if (callRes.status !== 200) {
|
|
97
|
+
throw new Error(`Socket MCP depscore failed: HTTP ${callRes.status}`);
|
|
98
|
+
}
|
|
99
|
+
const parsed = JSON.parse(callRes.body);
|
|
100
|
+
if (parsed.error) {
|
|
101
|
+
throw new Error(`Socket MCP error: ${parsed.error.message ?? JSON.stringify(parsed.error)}`);
|
|
102
|
+
}
|
|
103
|
+
const content = parsed.result?.content;
|
|
104
|
+
if (!Array.isArray(content) || content.length === 0) {
|
|
105
|
+
throw new Error('Socket MCP returned empty content');
|
|
106
|
+
}
|
|
107
|
+
const text = content[0]?.text;
|
|
108
|
+
if (typeof text !== 'string') {
|
|
109
|
+
throw new Error('Socket MCP returned non-text content');
|
|
110
|
+
}
|
|
111
|
+
return parseScoresFromText(text);
|
|
112
|
+
}
|
|
113
|
+
//# sourceMappingURL=socket-client.js.map
|