@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.
Files changed (103) hide show
  1. package/LICENSE +77 -0
  2. package/README.md +120 -0
  3. package/bin/vibedefend.js +19 -0
  4. package/dist/auth/auth-store.js +170 -0
  5. package/dist/auth/auth-store.js.map +1 -0
  6. package/dist/auth/auth.js +125 -0
  7. package/dist/auth/auth.js.map +1 -0
  8. package/dist/auth/callback-server.js +216 -0
  9. package/dist/auth/callback-server.js.map +1 -0
  10. package/dist/auth/pkce.js +31 -0
  11. package/dist/auth/pkce.js.map +1 -0
  12. package/dist/auth/token-exchange.js +83 -0
  13. package/dist/auth/token-exchange.js.map +1 -0
  14. package/dist/clients/claude-code.js +170 -0
  15. package/dist/clients/claude-code.js.map +1 -0
  16. package/dist/clients/codex.js +378 -0
  17. package/dist/clients/codex.js.map +1 -0
  18. package/dist/clients/cursor-guards-rules.js +94 -0
  19. package/dist/clients/cursor-guards-rules.js.map +1 -0
  20. package/dist/clients/cursor.js +172 -0
  21. package/dist/clients/cursor.js.map +1 -0
  22. package/dist/clients/detect.js +86 -0
  23. package/dist/clients/detect.js.map +1 -0
  24. package/dist/clients/registry.js +41 -0
  25. package/dist/clients/registry.js.map +1 -0
  26. package/dist/clients/types.js +2 -0
  27. package/dist/clients/types.js.map +1 -0
  28. package/dist/clients/vscode.js +187 -0
  29. package/dist/clients/vscode.js.map +1 -0
  30. package/dist/clients/windsurf.js +151 -0
  31. package/dist/clients/windsurf.js.map +1 -0
  32. package/dist/config.js +32 -0
  33. package/dist/config.js.map +1 -0
  34. package/dist/custom-regions.js +112 -0
  35. package/dist/custom-regions.js.map +1 -0
  36. package/dist/diagnostics.js +122 -0
  37. package/dist/diagnostics.js.map +1 -0
  38. package/dist/doctor.js +125 -0
  39. package/dist/doctor.js.map +1 -0
  40. package/dist/guards-evaluator/bucketing.js +83 -0
  41. package/dist/guards-evaluator/bucketing.js.map +1 -0
  42. package/dist/guards-evaluator/evaluate.js +272 -0
  43. package/dist/guards-evaluator/evaluate.js.map +1 -0
  44. package/dist/guards-evaluator/glob.js +148 -0
  45. package/dist/guards-evaluator/glob.js.map +1 -0
  46. package/dist/guards-evaluator/index.js +9 -0
  47. package/dist/guards-evaluator/index.js.map +1 -0
  48. package/dist/guards-evaluator/preprocess.js +174 -0
  49. package/dist/guards-evaluator/preprocess.js.map +1 -0
  50. package/dist/guards-evaluator/redact.js +111 -0
  51. package/dist/guards-evaluator/redact.js.map +1 -0
  52. package/dist/guards-evaluator/regex.js +125 -0
  53. package/dist/guards-evaluator/regex.js.map +1 -0
  54. package/dist/guards-evaluator/types.js +2 -0
  55. package/dist/guards-evaluator/types.js.map +1 -0
  56. package/dist/guards-evaluator/validation.js +115 -0
  57. package/dist/guards-evaluator/validation.js.map +1 -0
  58. package/dist/hook-runner.js +6680 -0
  59. package/dist/hooks/install.js +169 -0
  60. package/dist/hooks/install.js.map +1 -0
  61. package/dist/hooks/runtime/api.js +167 -0
  62. package/dist/hooks/runtime/api.js.map +1 -0
  63. package/dist/hooks/runtime/config.js +60 -0
  64. package/dist/hooks/runtime/config.js.map +1 -0
  65. package/dist/hooks/runtime/emit.js +45 -0
  66. package/dist/hooks/runtime/emit.js.map +1 -0
  67. package/dist/hooks/runtime/fetch-rules.js +154 -0
  68. package/dist/hooks/runtime/fetch-rules.js.map +1 -0
  69. package/dist/hooks/runtime/guard-rules-cache.js +217 -0
  70. package/dist/hooks/runtime/guard-rules-cache.js.map +1 -0
  71. package/dist/hooks/runtime/guard-violations-buffer.js +105 -0
  72. package/dist/hooks/runtime/guard-violations-buffer.js.map +1 -0
  73. package/dist/hooks/runtime/pre-compact.js +41 -0
  74. package/dist/hooks/runtime/pre-compact.js.map +1 -0
  75. package/dist/hooks/runtime/resolve.js +206 -0
  76. package/dist/hooks/runtime/resolve.js.map +1 -0
  77. package/dist/hooks/runtime/session-review.js +198 -0
  78. package/dist/hooks/runtime/session-review.js.map +1 -0
  79. package/dist/hooks/runtime/session-start.js +101 -0
  80. package/dist/hooks/runtime/session-start.js.map +1 -0
  81. package/dist/hooks/runtime/sniff.js +112 -0
  82. package/dist/hooks/runtime/sniff.js.map +1 -0
  83. package/dist/hooks/runtime/types.js +22 -0
  84. package/dist/hooks/runtime/types.js.map +1 -0
  85. package/dist/hooks/runtime/user-prompt-submit.js +154 -0
  86. package/dist/hooks/runtime/user-prompt-submit.js.map +1 -0
  87. package/dist/index.js +129 -0
  88. package/dist/index.js.map +1 -0
  89. package/dist/install.js +183 -0
  90. package/dist/install.js.map +1 -0
  91. package/dist/login.js +335 -0
  92. package/dist/login.js.map +1 -0
  93. package/dist/prompts.js +134 -0
  94. package/dist/prompts.js.map +1 -0
  95. package/dist/self-update.js +177 -0
  96. package/dist/self-update.js.map +1 -0
  97. package/dist/status.js +58 -0
  98. package/dist/status.js.map +1 -0
  99. package/dist/utils.js +84 -0
  100. package/dist/utils.js.map +1 -0
  101. package/dist/version.js +23 -0
  102. package/dist/version.js.map +1 -0
  103. 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":"AAAA;;;;;;;;;GASG;AACH,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AACnC,OAAO,EAAE,gBAAgB,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC9D,OAAO,EAAE,mBAAmB,EAAE,MAAM,UAAU,CAAC;AAC/C,OAAO,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AACxC,OAAO,EAAE,iBAAiB,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAW/D,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACpC,OAAyB,EAAE;IAE3B,MAAM,SAAS,GAAG,IAAI,CAAC,SAAS,IAAI,aAAa,CAAC;IAClD,MAAM,UAAU,GAAG,IAAI,CAAC,UAAU,IAAI,iBAAiB,CAAC;IACxD,MAAM,cAAc,GAAG,IAAI,CAAC,cAAc,IAAI,gBAAgB,CAAC;IAC/D,MAAM,UAAU,GAAG,IAAI,CAAC,cAAc,IAAI,YAAY,CAAC;IACvD,MAAM,cAAc,GAAG,IAAI,CAAC,gBAAgB,IAAI,mBAAmB,CAAC;IACpE,sEAAsE;IACtE,sEAAsE;IACtE,oEAAoE;IACpE,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,IAAI,WAAW,CAAC;IAE1C,MAAM,KAAK,GAAG,MAAM,SAAS,EAAE,CAAC;IAChC,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC;IAE3B,MAAM,MAAM,GAAG,UAAU,EAAE,CAAC;IAC5B,IAAI,CAAC,MAAM;QAAE,OAAO;IAEpB,MAAM,EAAE,SAAS,EAAE,OAAO,EAAE,aAAa,EAAE,GAAG,cAAc,EAAE,CAAC;IAC/D,IAAI,CAAC,SAAS;QAAE,OAAO;IAEvB,MAAM,OAAO,GACX,OAAO,CAAC,GAAG,CAAC,mBAAmB,IAAI,aAAa,IAAI,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC;IAE5E,oEAAoE;IACpE,IAAI,OAAO,CAAC,GAAG,CAAC,kBAAkB,KAAK,GAAG,EAAE,CAAC;QAC3C,MAAM,CACJ,oDAAoD,KAAK,CAAC,MAAM,iBAAiB,SAAS,IAAI,EAC9F,KAAK,CAAC,MAAM,CACb,CAAC;QACF,OAAO;IACT,CAAC;IAED,2EAA2E;IAC3E,uEAAuE;IACvE,mEAAmE;IACnE,uEAAuE;IACvE,oEAAoE;IACpE,sEAAsE;IACtE,4DAA4D;IAC5D,MAAM,KAAK,GAAG,UAAU,CAAC,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;IAChD,IAAI,KAAK,GAAkB,IAAI,CAAC;IAChC,IAAI,KAAK,EAAE,CAAC;QACV,MAAM,MAAM,GAAG,MAAM,cAAc,CAAC,EAAE,OAAO,EAAE,SAAS,EAAE,KAAK,EAAE,CAAC,CAAC;QACnE,IAAI,MAAM;YAAE,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC;IACnC,CAAC;IAED,MAAM,KAAK,GAAa;QACtB,iCAAiC;QACjC,gBAAgB,SAAS,eAAe,KAAK,CAAC,MAAM,GAAG;KACxD,CAAC;IACF,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;QACnB,KAAK,CAAC,IAAI,CAAC,0BAA0B,KAAK,IAAI,CAAC,CAAC;IAClD,CAAC;IACD,KAAK,CAAC,IAAI,CACR,gFAAgF,EAChF,mFAAmF,CACpF,CAAC;IAEF,iEAAiE;IACjE,oEAAoE;IACpE,qEAAqE;IACrE,kEAAkE;IAClE,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACf,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;QACnB,oEAAoE;QACpE,oEAAoE;QACpE,iEAAiE;QACjE,KAAK,CAAC,IAAI,CACR,8EAA8E,EAC9E,gFAAgF,EAChF,mEAAmE,CACpE,CAAC;IACJ,CAAC;SAAM,IAAI,KAAK,KAAK,CAAC,EAAE,CAAC;QACvB,KAAK,CAAC,IAAI,CACR,sFAAsF,CACvF,CAAC;IACJ,CAAC;SAAM,CAAC;QACN,KAAK,CAAC,IAAI,CACR,mCAAmC,KAAK,+BAA+B,EACvE,2EAA2E,EAC3E,gFAAgF,EAChF,8CAA8C,CAC/C,CAAC;IACJ,CAAC;IAED,6DAA6D;IAC7D,0EAA0E;IAC1E,uEAAuE;IACvE,wEAAwE;IACxE,sEAAsE;IACtE,0EAA0E;IAC1E,6EAA6E;IAC7E,EAAE;IACF,sEAAsE;IACtE,0EAA0E;IAC1E,mEAAmE;IACnE,MAAM,wBAAwB,GAAG,IAAI,GAAG,CAAC,CAAC,aAAa,EAAE,OAAO,EAAE,QAAQ,CAAC,CAAC,CAAC;IAE7E,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACf,IAAI,wBAAwB,CAAC,GAAG,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,CAAC;QAC/C,KAAK,CAAC,IAAI,CACR,wEAAwE,EACxE,uEAAuE,EACvE,wEAAwE,EACxE,wEAAwE,EACxE,gEAAgE,CACjE,CAAC;IACJ,CAAC;SAAM,CAAC;QACN,8CAA8C;QAC9C,KAAK,CAAC,IAAI,CACR,sEAAsE,EACtE,2DAA2D,EAC3D,yEAAyE,EACzE,+BAA+B,CAChC,CAAC;IACJ,CAAC;IAED,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,IAAI,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;AAChD,CAAC"}
@@ -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":"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"}
@@ -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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"}