@cybedefend/vibedefend 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +77 -0
- package/README.md +120 -0
- package/bin/vibedefend.js +19 -0
- package/dist/auth/auth-store.js +170 -0
- package/dist/auth/auth-store.js.map +1 -0
- package/dist/auth/auth.js +125 -0
- package/dist/auth/auth.js.map +1 -0
- package/dist/auth/callback-server.js +216 -0
- package/dist/auth/callback-server.js.map +1 -0
- package/dist/auth/pkce.js +31 -0
- package/dist/auth/pkce.js.map +1 -0
- package/dist/auth/token-exchange.js +83 -0
- package/dist/auth/token-exchange.js.map +1 -0
- package/dist/clients/claude-code.js +170 -0
- package/dist/clients/claude-code.js.map +1 -0
- package/dist/clients/codex.js +378 -0
- package/dist/clients/codex.js.map +1 -0
- package/dist/clients/cursor-guards-rules.js +94 -0
- package/dist/clients/cursor-guards-rules.js.map +1 -0
- package/dist/clients/cursor.js +172 -0
- package/dist/clients/cursor.js.map +1 -0
- package/dist/clients/detect.js +86 -0
- package/dist/clients/detect.js.map +1 -0
- package/dist/clients/registry.js +41 -0
- package/dist/clients/registry.js.map +1 -0
- package/dist/clients/types.js +2 -0
- package/dist/clients/types.js.map +1 -0
- package/dist/clients/vscode.js +187 -0
- package/dist/clients/vscode.js.map +1 -0
- package/dist/clients/windsurf.js +151 -0
- package/dist/clients/windsurf.js.map +1 -0
- package/dist/config.js +32 -0
- package/dist/config.js.map +1 -0
- package/dist/custom-regions.js +112 -0
- package/dist/custom-regions.js.map +1 -0
- package/dist/diagnostics.js +122 -0
- package/dist/diagnostics.js.map +1 -0
- package/dist/doctor.js +125 -0
- package/dist/doctor.js.map +1 -0
- package/dist/guards-evaluator/bucketing.js +83 -0
- package/dist/guards-evaluator/bucketing.js.map +1 -0
- package/dist/guards-evaluator/evaluate.js +272 -0
- package/dist/guards-evaluator/evaluate.js.map +1 -0
- package/dist/guards-evaluator/glob.js +148 -0
- package/dist/guards-evaluator/glob.js.map +1 -0
- package/dist/guards-evaluator/index.js +9 -0
- package/dist/guards-evaluator/index.js.map +1 -0
- package/dist/guards-evaluator/preprocess.js +174 -0
- package/dist/guards-evaluator/preprocess.js.map +1 -0
- package/dist/guards-evaluator/redact.js +111 -0
- package/dist/guards-evaluator/redact.js.map +1 -0
- package/dist/guards-evaluator/regex.js +125 -0
- package/dist/guards-evaluator/regex.js.map +1 -0
- package/dist/guards-evaluator/types.js +2 -0
- package/dist/guards-evaluator/types.js.map +1 -0
- package/dist/guards-evaluator/validation.js +115 -0
- package/dist/guards-evaluator/validation.js.map +1 -0
- package/dist/hook-runner.js +6680 -0
- package/dist/hooks/install.js +169 -0
- package/dist/hooks/install.js.map +1 -0
- package/dist/hooks/runtime/api.js +167 -0
- package/dist/hooks/runtime/api.js.map +1 -0
- package/dist/hooks/runtime/config.js +60 -0
- package/dist/hooks/runtime/config.js.map +1 -0
- package/dist/hooks/runtime/emit.js +45 -0
- package/dist/hooks/runtime/emit.js.map +1 -0
- package/dist/hooks/runtime/fetch-rules.js +154 -0
- package/dist/hooks/runtime/fetch-rules.js.map +1 -0
- package/dist/hooks/runtime/guard-rules-cache.js +217 -0
- package/dist/hooks/runtime/guard-rules-cache.js.map +1 -0
- package/dist/hooks/runtime/guard-violations-buffer.js +105 -0
- package/dist/hooks/runtime/guard-violations-buffer.js.map +1 -0
- package/dist/hooks/runtime/pre-compact.js +41 -0
- package/dist/hooks/runtime/pre-compact.js.map +1 -0
- package/dist/hooks/runtime/resolve.js +206 -0
- package/dist/hooks/runtime/resolve.js.map +1 -0
- package/dist/hooks/runtime/session-review.js +198 -0
- package/dist/hooks/runtime/session-review.js.map +1 -0
- package/dist/hooks/runtime/session-start.js +101 -0
- package/dist/hooks/runtime/session-start.js.map +1 -0
- package/dist/hooks/runtime/sniff.js +112 -0
- package/dist/hooks/runtime/sniff.js.map +1 -0
- package/dist/hooks/runtime/types.js +22 -0
- package/dist/hooks/runtime/types.js.map +1 -0
- package/dist/hooks/runtime/user-prompt-submit.js +154 -0
- package/dist/hooks/runtime/user-prompt-submit.js.map +1 -0
- package/dist/index.js +129 -0
- package/dist/index.js.map +1 -0
- package/dist/install.js +183 -0
- package/dist/install.js.map +1 -0
- package/dist/login.js +335 -0
- package/dist/login.js.map +1 -0
- package/dist/prompts.js +134 -0
- package/dist/prompts.js.map +1 -0
- package/dist/self-update.js +177 -0
- package/dist/self-update.js.map +1 -0
- package/dist/status.js +58 -0
- package/dist/status.js.map +1 -0
- package/dist/utils.js +84 -0
- package/dist/utils.js.map +1 -0
- package/dist/version.js +23 -0
- package/dist/version.js.map +1 -0
- package/package.json +73 -0
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stop / end-of-session gap-analysis hook.
|
|
3
|
+
*
|
|
4
|
+
* Counts how many code-mutating tool calls happened in the transcript;
|
|
5
|
+
* if `>= reviewThreshold`, emits the cybe-review directive that asks the
|
|
6
|
+
* agent to compare the user's prompt against the rules applied during
|
|
7
|
+
* the session and offer to propose missing ones.
|
|
8
|
+
*
|
|
9
|
+
* Transcript handling — every client gives us a path to a transcript
|
|
10
|
+
* file (JSONL for most, plain JSON array for some). We read it and
|
|
11
|
+
* scan for tool invocations regardless of the exact field names used
|
|
12
|
+
* (`tool_uses`, `toolCalls`, `tools`, etc.).
|
|
13
|
+
*/
|
|
14
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
15
|
+
import { sniff } from './sniff.js';
|
|
16
|
+
import { emit } from './emit.js';
|
|
17
|
+
import { loadRuntimeConfig, readStdinJson } from './config.js';
|
|
18
|
+
const COUNTED_TOOLS = new Set([
|
|
19
|
+
'Edit',
|
|
20
|
+
'Write',
|
|
21
|
+
'MultiEdit',
|
|
22
|
+
'apply_patch',
|
|
23
|
+
'pre_write_code',
|
|
24
|
+
]);
|
|
25
|
+
export async function sessionReviewHook(deps = {}) {
|
|
26
|
+
const readStdin = deps.readStdin ?? readStdinJson;
|
|
27
|
+
const loadConfig = deps.loadConfig ?? loadRuntimeConfig;
|
|
28
|
+
const emitFn = deps.emitFn ?? emit;
|
|
29
|
+
const readTranscriptFile = deps.readTranscriptFile ?? defaultReadTranscript;
|
|
30
|
+
const stdin = await readStdin();
|
|
31
|
+
const event = sniff(stdin);
|
|
32
|
+
const config = loadConfig();
|
|
33
|
+
if (!config)
|
|
34
|
+
return;
|
|
35
|
+
if (!config.hooks.enableSessionReview)
|
|
36
|
+
return;
|
|
37
|
+
const transcript = loadTranscript(stdin, readTranscriptFile);
|
|
38
|
+
let editCount = countEdits(transcript);
|
|
39
|
+
// Dry-run override for hook-debug — pretend the session had N edits.
|
|
40
|
+
if (process.env.CYBEDEFEND_DRY_RUN === '1') {
|
|
41
|
+
editCount = parseInt(process.env.CYBEDEFEND_DRY_RUN_EDITS ?? '5', 10);
|
|
42
|
+
}
|
|
43
|
+
if (editCount === 0)
|
|
44
|
+
return;
|
|
45
|
+
if (editCount < config.hooks.reviewThreshold)
|
|
46
|
+
return;
|
|
47
|
+
const userPrompt = extractFirstUserPrompt(transcript);
|
|
48
|
+
const body = composeReviewBody(editCount, event.client, userPrompt, config.hooks.autoProposeMode);
|
|
49
|
+
emitFn(body, event.client);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Resolve the transcript to a JS array of "events". Each client packages
|
|
53
|
+
* it differently:
|
|
54
|
+
* - Inline `transcript: [...]` in stdin (Claude Code Stop)
|
|
55
|
+
* - `transcript_path: "..."` pointing to a JSONL or JSON-array file
|
|
56
|
+
*/
|
|
57
|
+
function loadTranscript(stdin, readFile) {
|
|
58
|
+
if (Array.isArray(stdin.transcript))
|
|
59
|
+
return stdin.transcript;
|
|
60
|
+
const path = typeof stdin.transcript_path === 'string' ? stdin.transcript_path : '';
|
|
61
|
+
if (!path)
|
|
62
|
+
return [];
|
|
63
|
+
const raw = readFile(path);
|
|
64
|
+
if (!raw)
|
|
65
|
+
return [];
|
|
66
|
+
// Try JSONL first (one JSON object per line), then a single JSON array.
|
|
67
|
+
const trimmed = raw.trim();
|
|
68
|
+
if (trimmed.startsWith('[')) {
|
|
69
|
+
try {
|
|
70
|
+
const arr = JSON.parse(trimmed);
|
|
71
|
+
return Array.isArray(arr) ? arr : [];
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
return [];
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
const out = [];
|
|
78
|
+
for (const line of trimmed.split('\n')) {
|
|
79
|
+
const l = line.trim();
|
|
80
|
+
if (!l)
|
|
81
|
+
continue;
|
|
82
|
+
try {
|
|
83
|
+
out.push(JSON.parse(l));
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
// Skip malformed lines — agent transcripts occasionally have
|
|
87
|
+
// partial writes during compaction. Continue rather than abort.
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return out;
|
|
91
|
+
}
|
|
92
|
+
function defaultReadTranscript(path) {
|
|
93
|
+
if (!existsSync(path))
|
|
94
|
+
return null;
|
|
95
|
+
try {
|
|
96
|
+
return readFileSync(path, 'utf8');
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Walk every event in the transcript and count tool invocations whose
|
|
104
|
+
* name is in COUNTED_TOOLS. Tolerant to the multiple field-name
|
|
105
|
+
* conventions different agents use (`tool_uses` / `toolCalls` / `tools`,
|
|
106
|
+
* and inside each `name` / `tool_name` / `toolName`).
|
|
107
|
+
*/
|
|
108
|
+
export function countEdits(transcript) {
|
|
109
|
+
let n = 0;
|
|
110
|
+
for (const event of transcript) {
|
|
111
|
+
if (!event || typeof event !== 'object')
|
|
112
|
+
continue;
|
|
113
|
+
const rec = event;
|
|
114
|
+
const candidates = [];
|
|
115
|
+
for (const key of ['tool_uses', 'toolCalls', 'tools', 'message']) {
|
|
116
|
+
const v = rec[key];
|
|
117
|
+
if (Array.isArray(v))
|
|
118
|
+
candidates.push(...v);
|
|
119
|
+
else if (v && typeof v === 'object')
|
|
120
|
+
candidates.push(v);
|
|
121
|
+
}
|
|
122
|
+
// Some transcripts inline a `name` / `tool_name` directly on the event.
|
|
123
|
+
candidates.push(rec);
|
|
124
|
+
for (const c of candidates) {
|
|
125
|
+
if (!c || typeof c !== 'object')
|
|
126
|
+
continue;
|
|
127
|
+
const obj = c;
|
|
128
|
+
const name = (typeof obj.name === 'string' && obj.name) ||
|
|
129
|
+
(typeof obj.tool_name === 'string' && obj.tool_name) ||
|
|
130
|
+
(typeof obj.toolName === 'string' && obj.toolName) ||
|
|
131
|
+
'';
|
|
132
|
+
if (COUNTED_TOOLS.has(name))
|
|
133
|
+
n += 1;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return n;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Pull the first user-role message text from the transcript. Truncated
|
|
140
|
+
* to 600 chars — enough context for the agent to do the gap analysis
|
|
141
|
+
* without bloating the output.
|
|
142
|
+
*/
|
|
143
|
+
export function extractFirstUserPrompt(transcript) {
|
|
144
|
+
for (const event of transcript) {
|
|
145
|
+
if (!event || typeof event !== 'object')
|
|
146
|
+
continue;
|
|
147
|
+
const rec = event;
|
|
148
|
+
const isUser = rec.role === 'user' || rec.type === 'user' || rec.from === 'user';
|
|
149
|
+
if (!isUser)
|
|
150
|
+
continue;
|
|
151
|
+
const text = (typeof rec.content === 'string' && rec.content) ||
|
|
152
|
+
(typeof rec.text === 'string' && rec.text) ||
|
|
153
|
+
(typeof rec.message === 'string' && rec.message) ||
|
|
154
|
+
'';
|
|
155
|
+
if (text)
|
|
156
|
+
return text.slice(0, 600);
|
|
157
|
+
}
|
|
158
|
+
return '';
|
|
159
|
+
}
|
|
160
|
+
function composeReviewBody(editCount, client, userPrompt, autoPropose) {
|
|
161
|
+
const safePrompt = userPrompt || '(could not extract — re-read transcript)';
|
|
162
|
+
const proposalLine = autoPropose
|
|
163
|
+
? [
|
|
164
|
+
' **Auto-propose mode is ON** (configured at install). Without asking the user,',
|
|
165
|
+
' call `cybe_rules_report_missing(projectId, { rule_text, evidence_files,',
|
|
166
|
+
' rationale: "<quote user prompt>", confirmed_by_user: true })` for each',
|
|
167
|
+
' uncovered concern. The user will validate them at the next session start',
|
|
168
|
+
' via the Accept/Reject picker.',
|
|
169
|
+
].join('\n')
|
|
170
|
+
: [
|
|
171
|
+
' Ask the user via AskUserQuestion:',
|
|
172
|
+
' > "Your prompt mentioned <concern>, but no business rule covers it.',
|
|
173
|
+
' > Propose this rule for the project?" — Yes / No / Edit before proposing.',
|
|
174
|
+
' ONLY on explicit "yes": call `cybe_rules_report_missing(projectId,',
|
|
175
|
+
' { rule_text, evidence_files, rationale: "<quote user prompt>",',
|
|
176
|
+
' confirmed_by_user: true })`.',
|
|
177
|
+
].join('\n');
|
|
178
|
+
return [
|
|
179
|
+
`## 🛡️ CybeDefend gap analysis (Stop hook — ${editCount} edits this session, client=${client})`,
|
|
180
|
+
'',
|
|
181
|
+
'Do a **gap analysis** between the original user request and the',
|
|
182
|
+
'business rules that were applied during this session.',
|
|
183
|
+
'',
|
|
184
|
+
'**Original user prompt (first 600 chars):**',
|
|
185
|
+
`> ${safePrompt}`,
|
|
186
|
+
'',
|
|
187
|
+
'### Required next-turn action',
|
|
188
|
+
'1. Extract security/business concerns the prompt mentions.',
|
|
189
|
+
'2. List CybeDefend rules returned during the session.',
|
|
190
|
+
'3. For each concern WITHOUT a matching rule:',
|
|
191
|
+
proposalLine,
|
|
192
|
+
'4. If every concern is covered, say so:',
|
|
193
|
+
' "🛡️ CybeDefend gap analysis: every concern in your request was covered',
|
|
194
|
+
' by an existing rule. No new proposals."',
|
|
195
|
+
'',
|
|
196
|
+
].join('\n');
|
|
197
|
+
}
|
|
198
|
+
//# sourceMappingURL=session-review.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-review.js","sourceRoot":"","sources":["../../../src/hooks/runtime/session-review.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SessionStart hook — fetch the proposal-inbox count + nudge the agent
|
|
3
|
+
* to run through it before responding to the user.
|
|
4
|
+
*
|
|
5
|
+
* Maps to:
|
|
6
|
+
* - Claude Code, VS Code, Codex: SessionStart event
|
|
7
|
+
* - Cursor: sessionStart event
|
|
8
|
+
* - Windsurf: pre_user_prompt (fires every turn; idempotent because
|
|
9
|
+
* the body only nudges if proposals exist)
|
|
10
|
+
*/
|
|
11
|
+
import { sniff } from './sniff.js';
|
|
12
|
+
import { resolveProjectId, resolveToken } from './resolve.js';
|
|
13
|
+
import { fetchProposalsCount } from './api.js';
|
|
14
|
+
import { emitContext } from './emit.js';
|
|
15
|
+
import { loadRuntimeConfig, readStdinJson } from './config.js';
|
|
16
|
+
export async function sessionStartHook(deps = {}) {
|
|
17
|
+
const readStdin = deps.readStdin ?? readStdinJson;
|
|
18
|
+
const loadConfig = deps.loadConfig ?? loadRuntimeConfig;
|
|
19
|
+
const resolveProject = deps.resolveProject ?? resolveProjectId;
|
|
20
|
+
const resolveTok = deps.resolveTokenFn ?? resolveToken;
|
|
21
|
+
const fetchProposals = deps.fetchProposalsFn ?? fetchProposalsCount;
|
|
22
|
+
// SessionStart context goes through `emitContext`, not `emit`, so the
|
|
23
|
+
// body lands inside Codex's `additionalContext` JSON envelope (and as
|
|
24
|
+
// plain markdown for everyone else). See emit.ts for the rationale.
|
|
25
|
+
const emitFn = deps.emitFn ?? emitContext;
|
|
26
|
+
const stdin = await readStdin();
|
|
27
|
+
const event = sniff(stdin);
|
|
28
|
+
const config = loadConfig();
|
|
29
|
+
if (!config)
|
|
30
|
+
return;
|
|
31
|
+
const { projectId, apiBase: configApiBase } = resolveProject();
|
|
32
|
+
if (!projectId)
|
|
33
|
+
return;
|
|
34
|
+
const apiBase = process.env.CYBEDEFEND_API_BASE ?? configApiBase ?? config.region.apiBase;
|
|
35
|
+
// Dry-run for hook-debug — emit a sentinel without hitting the API.
|
|
36
|
+
if (process.env.CYBEDEFEND_DRY_RUN === '1') {
|
|
37
|
+
emitFn(`## CybeDefend session bootstrap (dry-run, client=${event.client})\n- project: ${projectId}\n`, event.client);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
// Token + proposal-count are best-effort. The projectId block MUST surface
|
|
41
|
+
// even when neither is available — that is the most important piece of
|
|
42
|
+
// session context for the agent (especially under Codex, whose MCP
|
|
43
|
+
// credentials live in a different Keychain service that `resolveToken`
|
|
44
|
+
// doesn't read yet, and so token lookup returns null on every Codex
|
|
45
|
+
// session). The proposal-count line is omitted on failure rather than
|
|
46
|
+
// showing a misleading "0" or blocking the whole bootstrap.
|
|
47
|
+
const token = resolveTok(config.region.mcpName);
|
|
48
|
+
let total = null;
|
|
49
|
+
if (token) {
|
|
50
|
+
const result = await fetchProposals({ apiBase, projectId, token });
|
|
51
|
+
if (result)
|
|
52
|
+
total = result.total;
|
|
53
|
+
}
|
|
54
|
+
const lines = [
|
|
55
|
+
'## CybeDefend session bootstrap',
|
|
56
|
+
`- project: \`${projectId}\` (client: ${event.client})`,
|
|
57
|
+
];
|
|
58
|
+
if (total !== null) {
|
|
59
|
+
lines.push(`- pending proposals: **${total}**`);
|
|
60
|
+
}
|
|
61
|
+
lines.push('- (workflow doctrine is auto-injected via the MCP `Server.instructions` field;', ' if your client does not surface it, load the `cybe-init` MCP prompt manually.)');
|
|
62
|
+
// Part 1: Always emit an explicit Step 0 — proposals inbox line.
|
|
63
|
+
// The model's context-window scan picks up named anchors ("Step 0")
|
|
64
|
+
// much more reliably than generic doctrine paragraphs, especially on
|
|
65
|
+
// Codex where the token is not yet available so total stays null.
|
|
66
|
+
lines.push('');
|
|
67
|
+
if (total === null) {
|
|
68
|
+
// Token unavailable (Codex with unresolvable keychain, or transient
|
|
69
|
+
// API failure) — emit a strong "YOU MUST call" warning so the model
|
|
70
|
+
// still has a clear Step 0 signal even without a concrete count.
|
|
71
|
+
lines.push('- **Step 0 — proposals inbox**: ⚠ unable to verify (no MCP token reachable).', ' **YOU MUST call `cybe_rules_list_proposals(projectId, status="proposed")` as', ' your VERY FIRST action this session, before any project work.**');
|
|
72
|
+
}
|
|
73
|
+
else if (total === 0) {
|
|
74
|
+
lines.push('- **Step 0 — proposals inbox**: 0 pending. ✓ Bootstrap complete; no action required.');
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
lines.push(`- **Step 0 — proposals inbox**: ${total} pending. ⚠ ACTION REQUIRED —`, ' list them via `cybe_rules_list_proposals(projectId, status="proposed")`', ' and surface each via AskUserQuestion (Accept / Reject / Skip / Tell me more)', ' **before responding to the user request.**');
|
|
78
|
+
}
|
|
79
|
+
// Part 3: Action Guards reminder — message varies by client.
|
|
80
|
+
// Hard-enforcement clients (Claude Code, Codex, Cursor) have a PreToolUse
|
|
81
|
+
// hook that exits with code 2 on deny, which hard-blocks the tool call
|
|
82
|
+
// BEFORE the LLM executes it. Pushing the LLM to call cybe_guards_check
|
|
83
|
+
// preemptively dilutes that UX: the user sees a polite "I checked and
|
|
84
|
+
// refused" instead of a clean runtime intercept. On those clients the LLM
|
|
85
|
+
// should do nothing — just make the tool call and let the hook intercept it.
|
|
86
|
+
//
|
|
87
|
+
// Windsurf's pre_write_code hook only covers file writes. Read, Bash,
|
|
88
|
+
// WebFetch, and network actions are NOT intercepted at the hook level, so
|
|
89
|
+
// the LLM must still pre-check those itself via cybe_guards_check.
|
|
90
|
+
const HARD_ENFORCEMENT_CLIENTS = new Set(['claude-code', 'codex', 'cursor']);
|
|
91
|
+
lines.push('');
|
|
92
|
+
if (HARD_ENFORCEMENT_CLIENTS.has(event.client)) {
|
|
93
|
+
lines.push('- **Action Guards** are enforced silently by a PreToolUse hook on this', ' client. The hook intercepts your tool calls before they execute and', ' blocks any that match a deny rule. You do NOT need to call any guard', ' tool preemptively — just make your tool call. If the hook blocks it,', ' you will see a stderr message; do not retry the same action.');
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
// Windsurf and other partial-coverage clients
|
|
97
|
+
lines.push('- **Action Guards** — your runtime hook only covers file writes. For', ' Read / Bash / WebFetch / network actions, you MUST call', ' `cybe_guards_check(projectId, agent, tool, target)` BEFORE the action', ' and respect a deny verdict.');
|
|
98
|
+
}
|
|
99
|
+
emitFn(lines.join('\n') + '\n', event.client);
|
|
100
|
+
}
|
|
101
|
+
//# sourceMappingURL=session-start.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-start.js","sourceRoot":"","sources":["../../../src/hooks/runtime/session-start.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/** Extract a file path from a unified-diff patch (Codex `apply_patch`). */
|
|
2
|
+
function filePathFromPatch(patch) {
|
|
3
|
+
const lines = patch.split('\n');
|
|
4
|
+
for (const line of lines) {
|
|
5
|
+
const updateMatch = /^\*\*\* (?:Update|Add|Create) File: (.+)$/.exec(line);
|
|
6
|
+
if (updateMatch)
|
|
7
|
+
return updateMatch[1];
|
|
8
|
+
const pmMatch = /^\+\+\+ b\/(.+)$/.exec(line);
|
|
9
|
+
if (pmMatch)
|
|
10
|
+
return pmMatch[1];
|
|
11
|
+
}
|
|
12
|
+
return '';
|
|
13
|
+
}
|
|
14
|
+
/** Concatenate `new_string` from a MultiEdit `edits` array. */
|
|
15
|
+
function joinMultiEditNewStrings(edits) {
|
|
16
|
+
if (!Array.isArray(edits))
|
|
17
|
+
return '';
|
|
18
|
+
return edits
|
|
19
|
+
.map((e) => (typeof e.new_string === 'string' ? e.new_string : ''))
|
|
20
|
+
.filter((s) => s.length > 0)
|
|
21
|
+
.join('\n---\n');
|
|
22
|
+
}
|
|
23
|
+
/** Concatenate Windsurf-shaped edits. */
|
|
24
|
+
function joinWindsurfEdits(edits) {
|
|
25
|
+
return joinMultiEditNewStrings(edits);
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Pure function: maps an unparsed stdin object to a NormalizedEvent.
|
|
29
|
+
*
|
|
30
|
+
* Returns a NormalizedEvent with `client: 'unknown'` if no schema matches,
|
|
31
|
+
* so the caller can early-exit without crashing.
|
|
32
|
+
*/
|
|
33
|
+
export function sniff(input) {
|
|
34
|
+
// ── Windsurf ─────────────────────────────────────────────────────────
|
|
35
|
+
if (typeof input.agent_action_name === 'string') {
|
|
36
|
+
const action = input.agent_action_name;
|
|
37
|
+
const toolInfo = input.tool_info ?? {};
|
|
38
|
+
const filePath = typeof toolInfo.file_path === 'string' ? toolInfo.file_path : '';
|
|
39
|
+
const newContent = joinWindsurfEdits(toolInfo.edits);
|
|
40
|
+
let toolName = action;
|
|
41
|
+
if (action === 'pre_write_code' || action === 'post_write_code') {
|
|
42
|
+
toolName = 'Write';
|
|
43
|
+
}
|
|
44
|
+
else if (action === 'pre_read_code' || action === 'post_read_code') {
|
|
45
|
+
toolName = 'Read';
|
|
46
|
+
}
|
|
47
|
+
return { client: 'windsurf', toolName, filePath, newContent, raw: input };
|
|
48
|
+
}
|
|
49
|
+
// ── VS Code Copilot ──────────────────────────────────────────────────
|
|
50
|
+
// VS Code uses camelCase field name `hookEventName` (distinct from
|
|
51
|
+
// Claude Code/Cursor/Codex's snake_case `hook_event_name`).
|
|
52
|
+
if ('hookEventName' in input) {
|
|
53
|
+
return extractToolInputShape(input, 'vscode');
|
|
54
|
+
}
|
|
55
|
+
// ── Cursor / Codex / Claude Code (shared shape) ──────────────────────
|
|
56
|
+
if ('hook_event_name' in input) {
|
|
57
|
+
const evt = typeof input.hook_event_name === 'string' ? input.hook_event_name : '';
|
|
58
|
+
let client;
|
|
59
|
+
if (/^[a-z]/.test(evt)) {
|
|
60
|
+
// Cursor uses camelCase event values (`preToolUse`).
|
|
61
|
+
client = 'cursor';
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
// Codex emits `apply_patch` as a tool name (with patch payload);
|
|
65
|
+
// Claude Code uses Edit/Write/MultiEdit. Disambiguate via tool_name.
|
|
66
|
+
const toolName = typeof input.tool_name === 'string' ? input.tool_name : '';
|
|
67
|
+
client = toolName === 'apply_patch' ? 'codex' : 'claude-code';
|
|
68
|
+
}
|
|
69
|
+
return extractToolInputShape(input, client);
|
|
70
|
+
}
|
|
71
|
+
// ── Nothing matched ──────────────────────────────────────────────────
|
|
72
|
+
return {
|
|
73
|
+
client: 'unknown',
|
|
74
|
+
toolName: '',
|
|
75
|
+
filePath: '',
|
|
76
|
+
newContent: '',
|
|
77
|
+
raw: input,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Shared extraction for the Claude-Code-shaped families (Cursor / Codex /
|
|
82
|
+
* Claude Code / VS Code Copilot). Pulls `tool_name` + `tool_input.*`,
|
|
83
|
+
* deriving the file path / new content depending on which tool fired.
|
|
84
|
+
*/
|
|
85
|
+
function extractToolInputShape(input, client) {
|
|
86
|
+
const toolName = typeof input.tool_name === 'string' ? input.tool_name : '';
|
|
87
|
+
const toolInput = input.tool_input ?? {};
|
|
88
|
+
let filePath = typeof toolInput.file_path === 'string' ? toolInput.file_path : '';
|
|
89
|
+
let newContent = '';
|
|
90
|
+
switch (toolName) {
|
|
91
|
+
case 'Write':
|
|
92
|
+
newContent =
|
|
93
|
+
typeof toolInput.content === 'string' ? toolInput.content : '';
|
|
94
|
+
break;
|
|
95
|
+
case 'Edit':
|
|
96
|
+
newContent =
|
|
97
|
+
typeof toolInput.new_string === 'string' ? toolInput.new_string : '';
|
|
98
|
+
break;
|
|
99
|
+
case 'MultiEdit':
|
|
100
|
+
newContent = joinMultiEditNewStrings(toolInput.edits);
|
|
101
|
+
break;
|
|
102
|
+
case 'apply_patch': {
|
|
103
|
+
const patch = typeof toolInput.patch === 'string' ? toolInput.patch : '';
|
|
104
|
+
newContent = patch;
|
|
105
|
+
if (!filePath)
|
|
106
|
+
filePath = filePathFromPatch(patch);
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return { client, toolName, filePath, newContent, raw: input };
|
|
111
|
+
}
|
|
112
|
+
//# sourceMappingURL=sniff.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sniff.js","sourceRoot":"","sources":["../../../src/hooks/runtime/sniff.ts"],"names":[],"mappings":"AAiBA,2EAA2E;AAC3E,SAAS,iBAAiB,CAAC,KAAa;IACtC,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAChC,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,MAAM,WAAW,GAAG,2CAA2C,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC3E,IAAI,WAAW;YAAE,OAAO,WAAW,CAAC,CAAC,CAAC,CAAC;QACvC,MAAM,OAAO,GAAG,kBAAkB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC9C,IAAI,OAAO;YAAE,OAAO,OAAO,CAAC,CAAC,CAAC,CAAC;IACjC,CAAC;IACD,OAAO,EAAE,CAAC;AACZ,CAAC;AAED,+DAA+D;AAC/D,SAAS,uBAAuB,CAC9B,KAAiD;IAEjD,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QAAE,OAAO,EAAE,CAAC;IACrC,OAAO,KAAK;SACT,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC,UAAU,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;SAClE,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC;SAC3B,IAAI,CAAC,SAAS,CAAC,CAAC;AACrB,CAAC;AAED,yCAAyC;AACzC,SAAS,iBAAiB,CACxB,KAAiD;IAEjD,OAAO,uBAAuB,CAAC,KAAK,CAAC,CAAC;AACxC,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,KAAK,CAAC,KAA8B;IAClD,wEAAwE;IACxE,IAAI,OAAO,KAAK,CAAC,iBAAiB,KAAK,QAAQ,EAAE,CAAC;QAChD,MAAM,MAAM,GAAG,KAAK,CAAC,iBAAiB,CAAC;QACvC,MAAM,QAAQ,GAAI,KAAK,CAAC,SAAqC,IAAI,EAAE,CAAC;QACpE,MAAM,QAAQ,GACZ,OAAO,QAAQ,CAAC,SAAS,KAAK,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC;QACnE,MAAM,UAAU,GAAG,iBAAiB,CAClC,QAAQ,CAAC,KAAmD,CAC7D,CAAC;QACF,IAAI,QAAQ,GAAG,MAAM,CAAC;QACtB,IAAI,MAAM,KAAK,gBAAgB,IAAI,MAAM,KAAK,iBAAiB,EAAE,CAAC;YAChE,QAAQ,GAAG,OAAO,CAAC;QACrB,CAAC;aAAM,IAAI,MAAM,KAAK,eAAe,IAAI,MAAM,KAAK,gBAAgB,EAAE,CAAC;YACrE,QAAQ,GAAG,MAAM,CAAC;QACpB,CAAC;QACD,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,QAAQ,EAAE,QAAQ,EAAE,UAAU,EAAE,GAAG,EAAE,KAAK,EAAE,CAAC;IAC5E,CAAC;IAED,wEAAwE;IACxE,mEAAmE;IACnE,4DAA4D;IAC5D,IAAI,eAAe,IAAI,KAAK,EAAE,CAAC;QAC7B,OAAO,qBAAqB,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAC;IAChD,CAAC;IAED,wEAAwE;IACxE,IAAI,iBAAiB,IAAI,KAAK,EAAE,CAAC;QAC/B,MAAM,GAAG,GACP,OAAO,KAAK,CAAC,eAAe,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,eAAe,CAAC,CAAC,CAAC,EAAE,CAAC;QACzE,IAAI,MAAgB,CAAC;QACrB,IAAI,QAAQ,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;YACvB,qDAAqD;YACrD,MAAM,GAAG,QAAQ,CAAC;QACpB,CAAC;aAAM,CAAC;YACN,iEAAiE;YACjE,qEAAqE;YACrE,MAAM,QAAQ,GACZ,OAAO,KAAK,CAAC,SAAS,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC;YAC7D,MAAM,GAAG,QAAQ,KAAK,aAAa,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,aAAa,CAAC;QAChE,CAAC;QACD,OAAO,qBAAqB,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;IAC9C,CAAC;IAED,wEAAwE;IACxE,OAAO;QACL,MAAM,EAAE,SAAS;QACjB,QAAQ,EAAE,EAAE;QACZ,QAAQ,EAAE,EAAE;QACZ,UAAU,EAAE,EAAE;QACd,GAAG,EAAE,KAAK;KACX,CAAC;AACJ,CAAC;AAED;;;;GAIG;AACH,SAAS,qBAAqB,CAC5B,KAA8B,EAC9B,MAAgB;IAEhB,MAAM,QAAQ,GACZ,OAAO,KAAK,CAAC,SAAS,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC;IAC7D,MAAM,SAAS,GAAI,KAAK,CAAC,UAAsC,IAAI,EAAE,CAAC;IACtE,IAAI,QAAQ,GACV,OAAO,SAAS,CAAC,SAAS,KAAK,QAAQ,CAAC,CAAC,CAAC,SAAS,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC;IACrE,IAAI,UAAU,GAAG,EAAE,CAAC;IAEpB,QAAQ,QAAQ,EAAE,CAAC;QACjB,KAAK,OAAO;YACV,UAAU;gBACR,OAAO,SAAS,CAAC,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC;YACjE,MAAM;QACR,KAAK,MAAM;YACT,UAAU;gBACR,OAAO,SAAS,CAAC,UAAU,KAAK,QAAQ,CAAC,CAAC,CAAC,SAAS,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,CAAC;YACvE,MAAM;QACR,KAAK,WAAW;YACd,UAAU,GAAG,uBAAuB,CAClC,SAAS,CAAC,KAAmD,CAC9D,CAAC;YACF,MAAM;QACR,KAAK,aAAa,CAAC,CAAC,CAAC;YACnB,MAAM,KAAK,GACT,OAAO,SAAS,CAAC,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC;YAC7D,UAAU,GAAG,KAAK,CAAC;YACnB,IAAI,CAAC,QAAQ;gBAAE,QAAQ,GAAG,iBAAiB,CAAC,KAAK,CAAC,CAAC;YACnD,MAAM;QACR,CAAC;IACH,CAAC;IAED,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,QAAQ,EAAE,UAAU,EAAE,GAAG,EAAE,KAAK,EAAE,CAAC;AAChE,CAAC"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared types for the Node-based hook runtime.
|
|
3
|
+
*
|
|
4
|
+
* The runtime replaces the previous bash hook scripts entirely. One
|
|
5
|
+
* bundled `dist/hook-runner.js` is copied to `~/.cybedefend/hook-runner.js`
|
|
6
|
+
* at install time and invoked by every AI agent via:
|
|
7
|
+
*
|
|
8
|
+
* node ~/.cybedefend/hook-runner.js <subcommand>
|
|
9
|
+
*
|
|
10
|
+
* Subcommands: fetch-rules | session-start | session-review | pre-compact
|
|
11
|
+
*
|
|
12
|
+
* Why Node instead of bash:
|
|
13
|
+
* - Cross-platform: Windows native (no Git Bash needed), Linux, macOS
|
|
14
|
+
* - Zero shell deps: no jq, no curl, no awk; uses Node's built-in fetch
|
|
15
|
+
* + JSON.parse + native HTTP
|
|
16
|
+
* - Testable: each module is a pure function, unit-tested with vitest
|
|
17
|
+
* instead of `bash -n` subprocess + apostrophe-in-heredoc whack-a-mole
|
|
18
|
+
* - Robust: JSON parsing handles unicode, multi-line strings, escapes —
|
|
19
|
+
* edges that fail silently with `jq -r` + bash variable interpolation
|
|
20
|
+
*/
|
|
21
|
+
export {};
|
|
22
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../../src/hooks/runtime/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG"}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UserPromptSubmit hook — Claude Code only.
|
|
3
|
+
*
|
|
4
|
+
* Fires before the agent processes each user prompt. We use it to force
|
|
5
|
+
* the CybeDefend workflow doctrine to surface BEFORE any skill (e.g.
|
|
6
|
+
* `superpowers:brainstorming`) hijacks the flow. Observed gap on Claude
|
|
7
|
+
* Code: the brainstorming skill auto-activates on task descriptions and
|
|
8
|
+
* takes precedence over MCP `Server.instructions` (which carries the
|
|
9
|
+
* doctrine), so the agent explores + asks clarifying questions WITHOUT
|
|
10
|
+
* ever calling `cybe_rules_list_proposals` or `cybe_rules_fetch`. Codex
|
|
11
|
+
* has no equivalent skill hijack, which is why it follows the doctrine
|
|
12
|
+
* directly — hence this hook is Claude-Code-scoped.
|
|
13
|
+
*
|
|
14
|
+
* Output is plain markdown to stdout (Claude Code's documented contract
|
|
15
|
+
* for UserPromptSubmit: stdout becomes additional context).
|
|
16
|
+
*
|
|
17
|
+
* Semantics:
|
|
18
|
+
* - Loud on empty: even when the proposal inbox is empty (total=0),
|
|
19
|
+
* we still emit a forceful reminder that the agent MUST explicitly
|
|
20
|
+
* acknowledge "No pending business-rule proposals" (satisfies step 0
|
|
21
|
+
* of the doctrine — silently skipping the step is what we observed
|
|
22
|
+
* and want to prevent).
|
|
23
|
+
* - If the API/token is unavailable, the doctrine still surfaces with
|
|
24
|
+
* a "could not verify, call cybe_rules_list_proposals yourself"
|
|
25
|
+
* fallback — never blocks the user's prompt.
|
|
26
|
+
* - When projectId can't be resolved (no `.cybedefend/config.json`),
|
|
27
|
+
* the hook is a silent no-op — there's no project context to anchor
|
|
28
|
+
* the doctrine against and emitting a half-finished reminder would
|
|
29
|
+
* confuse more than help.
|
|
30
|
+
*/
|
|
31
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
32
|
+
import { homedir } from 'node:os';
|
|
33
|
+
import { join } from 'node:path';
|
|
34
|
+
import { sniff } from './sniff.js';
|
|
35
|
+
import { resolveProjectId, resolveToken } from './resolve.js';
|
|
36
|
+
import { fetchProposalsCount } from './api.js';
|
|
37
|
+
import { emit } from './emit.js';
|
|
38
|
+
import { loadRuntimeConfig, readStdinJson } from './config.js';
|
|
39
|
+
/** Cap on persisted entries — drops oldest when exceeded. */
|
|
40
|
+
const SESSIONS_CAP = 100;
|
|
41
|
+
/** Default filename for the disk-backed tracker. */
|
|
42
|
+
function defaultSessionsFile() {
|
|
43
|
+
const dir = process.env.CYBEDEFEND_BASELINE_DIR ?? join(homedir(), '.cybedefend');
|
|
44
|
+
return join(dir, 'user-prompt-seen-sessions.json');
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Disk-backed SessionTracker. Best-effort — any FS error degrades to
|
|
48
|
+
* "haven't seen this session" which results in re-emitting the doctrine
|
|
49
|
+
* (loud is strictly better than silently swallowing it).
|
|
50
|
+
*/
|
|
51
|
+
function defaultSessionTracker(file = defaultSessionsFile()) {
|
|
52
|
+
function load() {
|
|
53
|
+
if (!existsSync(file))
|
|
54
|
+
return [];
|
|
55
|
+
try {
|
|
56
|
+
const raw = readFileSync(file, 'utf8');
|
|
57
|
+
const data = JSON.parse(raw);
|
|
58
|
+
if (!Array.isArray(data.sessions))
|
|
59
|
+
return [];
|
|
60
|
+
return data.sessions.filter((s) => typeof s === 'string');
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return []; // corrupt file → start over
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
hasSeen(sessionId) {
|
|
68
|
+
return load().includes(sessionId);
|
|
69
|
+
},
|
|
70
|
+
async markSeen(sessionId) {
|
|
71
|
+
const existing = load();
|
|
72
|
+
if (existing.includes(sessionId))
|
|
73
|
+
return;
|
|
74
|
+
const next = [...existing, sessionId].slice(-SESSIONS_CAP);
|
|
75
|
+
try {
|
|
76
|
+
mkdirSync(join(file, '..'), { recursive: true });
|
|
77
|
+
writeFileSync(file, JSON.stringify({ sessions: next }), 'utf8');
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
/* swallow — non-fatal */
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
export async function userPromptSubmitHook(deps = {}) {
|
|
86
|
+
const readStdin = deps.readStdin ?? readStdinJson;
|
|
87
|
+
const loadConfig = deps.loadConfig ?? loadRuntimeConfig;
|
|
88
|
+
const resolveProject = deps.resolveProject ?? resolveProjectId;
|
|
89
|
+
const resolveTok = deps.resolveTokenFn ?? resolveToken;
|
|
90
|
+
const fetchProposals = deps.fetchProposalsFn ?? fetchProposalsCount;
|
|
91
|
+
const emitFn = deps.emitFn ?? emit;
|
|
92
|
+
const sessionTracker = deps.sessionTracker ?? defaultSessionTracker();
|
|
93
|
+
const stdin = await readStdin();
|
|
94
|
+
const event = sniff(stdin);
|
|
95
|
+
// Per-session dedupe. If we have a session_id and we've already fired
|
|
96
|
+
// the doctrine for it, exit silently. If session_id is missing (older
|
|
97
|
+
// Claude Code build, malformed payload, etc.) we fall through and
|
|
98
|
+
// emit — loud is strictly better than silently dropping the doctrine.
|
|
99
|
+
const sessionId = typeof stdin.session_id === 'string' && stdin.session_id.length > 0
|
|
100
|
+
? stdin.session_id
|
|
101
|
+
: null;
|
|
102
|
+
if (sessionId !== null && sessionTracker.hasSeen(sessionId)) {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
const config = loadConfig();
|
|
106
|
+
if (!config)
|
|
107
|
+
return;
|
|
108
|
+
const { projectId, apiBase: configApiBase } = resolveProject();
|
|
109
|
+
if (!projectId)
|
|
110
|
+
return; // silent no-op — see file header
|
|
111
|
+
const apiBase = process.env.CYBEDEFEND_API_BASE ?? configApiBase ?? config.region.apiBase;
|
|
112
|
+
// Best-effort: token may not be available (Claude Code without an MCP
|
|
113
|
+
// OAuth dance against cybedefend-local, or transient Keychain failure).
|
|
114
|
+
// We still emit the doctrine — the agent calling
|
|
115
|
+
// `cybe_rules_list_proposals` itself satisfies step 0.
|
|
116
|
+
const token = resolveTok(config.region.mcpName);
|
|
117
|
+
let total = null;
|
|
118
|
+
if (token) {
|
|
119
|
+
const result = await fetchProposals({ apiBase, projectId, token });
|
|
120
|
+
if (result)
|
|
121
|
+
total = result.total;
|
|
122
|
+
}
|
|
123
|
+
const lines = [
|
|
124
|
+
'## ⚠️ CybeDefend doctrine — execute BEFORE answering this prompt',
|
|
125
|
+
'',
|
|
126
|
+
`**Project**: \`${projectId}\``,
|
|
127
|
+
'',
|
|
128
|
+
'### Step 0 — Proposal inbox',
|
|
129
|
+
];
|
|
130
|
+
if (total === null) {
|
|
131
|
+
lines.push('- CybeDefend: could not fetch proposal-inbox status (token / API unreachable).', '- Call `cybe_rules_list_proposals(projectId, status="proposed")` yourself to verify, then handle each surfaced proposal via AskUserQuestion (Accept / Reject / Skip / Tell me more) before continuing.');
|
|
132
|
+
}
|
|
133
|
+
else if (total > 0) {
|
|
134
|
+
lines.push(`- CybeDefend: **${total}** pending proposals.`, '- Call `cybe_rules_list_proposals(projectId, status="proposed")` and surface each via AskUserQuestion (Accept / Reject / Skip / Tell me more) BEFORE responding to this prompt.');
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
lines.push('- CybeDefend: **0** pending proposals.', '- You MUST still acknowledge step 0 in your reply ("No pending business-rule proposals."). Silently skipping this step is a doctrine violation observed in earlier sessions.');
|
|
138
|
+
}
|
|
139
|
+
lines.push('', '### Step 1 — Rules fetch (before any code edit)', '- Even if you plan to brainstorm / explore / ask clarifying questions first, call BOTH:', ' - `cybe_rules_fetch(projectId, files=[...], intent="<1-2 sentence narrative>")` for business rules', ' - `cybe_secrules_fetch(projectId, files=[...], intent="...", languages=[...])` for security rules', '- Use a rich `intent` — the retrieval is 60% graph + 40% vector, so a generic placeholder like "Editing file" returns nothing useful.', '- The PreToolUse hook fires automatically as a backstop on Edit/Write/MultiEdit, but your explicit call with a narrative intent gets better matches.', '', '### Order', '1. Satisfy step 0 (acknowledge / handle the inbox).', '2. Satisfy step 1 (fetch rules with a narrative intent) once you know which files you will touch.', '3. THEN proceed with whatever skill / brainstorm / implementation flow fits the task.', '');
|
|
140
|
+
emitFn(lines.join('\n'), event.client);
|
|
141
|
+
// Mark the session so subsequent prompts in the same conversation
|
|
142
|
+
// don't re-emit the doctrine. Best-effort — a write failure (FS
|
|
143
|
+
// permissions, disk full) just means the next prompt will re-emit,
|
|
144
|
+
// which is annoying but not broken.
|
|
145
|
+
if (sessionId !== null) {
|
|
146
|
+
try {
|
|
147
|
+
await sessionTracker.markSeen(sessionId);
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
/* swallow — non-fatal */
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
//# sourceMappingURL=user-prompt-submit.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"user-prompt-submit.js","sourceRoot":"","sources":["../../../src/hooks/runtime/user-prompt-submit.ts"],"names":[],"mappings":"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"}
|