@compr/opscontext-mcp 2.6.1 → 2.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +21 -0
- package/README.md +2 -2
- package/dist/cli-commands.js +1 -0
- package/dist/cli.js +8 -1
- package/dist/install-claude-hook.js +31 -6
- package/dist/session-gate.d.ts +32 -0
- package/dist/session-gate.js +167 -0
- package/package.json +1 -1
- package/skills/opscontext/SKILL.md +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,27 @@ All notable changes to OpsContext for AI Agents (previously ContextEngine — MC
|
|
|
4
4
|
|
|
5
5
|
> Entries for 2.2.0 through 2.4.0 were not backfilled here; see `docs/sessions/SESSION_19` through `SESSION_21` for those releases.
|
|
6
6
|
|
|
7
|
+
## [2.7.0] 2026-09-06: the session gate ships in the package
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- **`contextengine session-gate`** (`[SESSION-SAVE-IS-A-GATE]`, `src/session-gate.ts`): the body of
|
|
12
|
+
a Claude Code Stop hook. A turn cannot end (exit 2, reason on stderr) while the repo's CE session
|
|
13
|
+
(newest `~/.contextengine/sessions/*.json` whose name starts with the repo's name) is older than
|
|
14
|
+
the last commit. The message names the session to save, the newest `docs/sessions/SESSION_*.md`,
|
|
15
|
+
and how many `src/` commits `.github/copilot-instructions.md` is behind. Passes outside git,
|
|
16
|
+
before the first commit, and when Claude Code reports `stop_hook_active`. Why: the owner had to
|
|
17
|
+
type "check and update session and ce docs" at the end of every session; a reminder is not a rule.
|
|
18
|
+
- **`install-claude-hook` wires the gate** as a fourth entry (`Stop`) at user scope, through
|
|
19
|
+
`~/.claude/hooks/opscontext-session-gate.sh` with absolute node and CLI paths. Idempotent;
|
|
20
|
+
`uninstall-claude-hook` removes it with the others.
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
|
|
24
|
+
- **`install-claude-hook` crashed with "__dirname is not defined"** from the published package:
|
|
25
|
+
the module is ESM and looked up `defaults/` through a CommonJS global. It had never worked
|
|
26
|
+
outside the dev tree. Resolved through `import.meta.url` (`[M2-ESM-FILENAME-FIX]`).
|
|
27
|
+
|
|
7
28
|
## [2.6.1] 2026-09-06: readers never embed; a store record without tags no longer breaks search
|
|
8
29
|
|
|
9
30
|
### Fixed
|
package/README.md
CHANGED
|
@@ -165,7 +165,7 @@ Companion commands: `uninstall-autostart`, `autostart-status`.
|
|
|
165
165
|
npx @compr/opscontext-mcp install-claude-hook
|
|
166
166
|
```
|
|
167
167
|
|
|
168
|
-
Adds `UserPromptSubmit`, `PostToolUse`, and `SessionStart` hook entries to `~/.claude/settings.json` so every Claude Code prompt + tool call lands in the same audit log as the browser events.
|
|
168
|
+
Adds `UserPromptSubmit`, `PostToolUse`, and `SessionStart` hook entries to `~/.claude/settings.json` so every Claude Code prompt + tool call lands in the same audit log as the browser events, plus a `Stop` entry: the **session gate** (2.7.0). A Claude Code turn cannot end while the repo's OpsContext session is older than the last commit; the agent is told which session to save, which session doc to update, and how far the agent docs are behind. No more "did you save the session?" at the end of a day. Details: `npx @compr/opscontext-mcp session-gate --help`.
|
|
169
169
|
|
|
170
170
|
Verify:
|
|
171
171
|
```bash
|
|
@@ -525,7 +525,7 @@ Everything happens locally — search, scoring, learnings, sessions, embeddings.
|
|
|
525
525
|
| Learnings (operational rules) | `~/.contextengine/learnings.json` | ❌ Never |
|
|
526
526
|
| Sessions (decisions, progress) | `~/.contextengine/sessions/` | ❌ Never |
|
|
527
527
|
| Session stats (value meter) | `~/.contextengine/session-stats.json` | ❌ Never |
|
|
528
|
-
| Search index & embeddings | In-memory + `~/.contextengine/
|
|
528
|
+
| Search index & embeddings | In-memory + `~/.contextengine/embeddings.bin` (vectors) and `~/.contextengine/index/` (shared index) | ❌ Never |
|
|
529
529
|
| Git history & branches | Local `git` commands | ❌ Never |
|
|
530
530
|
| Dependencies & package.json | Read locally | ❌ Never |
|
|
531
531
|
| .env variable names | Read locally (values are never read) | ❌ Never |
|
package/dist/cli-commands.js
CHANGED
package/dist/cli.js
CHANGED
|
@@ -2548,7 +2548,8 @@ Usage:
|
|
|
2548
2548
|
contextengine install-autostart [--force]
|
|
2549
2549
|
Install macOS LaunchAgent so MCP server auto-starts at login
|
|
2550
2550
|
(uninstall-autostart / autostart-status — companion commands)
|
|
2551
|
-
contextengine install-claude-hook Wire Claude Code terminal sessions into the OpsContext audit log
|
|
2551
|
+
contextengine install-claude-hook Wire Claude Code terminal sessions into the OpsContext audit log, plus the Stop session gate
|
|
2552
|
+
contextengine session-gate Claude Code Stop hook body: exit 2 while the repo's CE session is older than HEAD
|
|
2552
2553
|
(UserPromptSubmit + PostToolUse + SessionStart hook entries)
|
|
2553
2554
|
contextengine watch [--json] [--severity info|warn|critical] [--once] [--window SECONDS]
|
|
2554
2555
|
Stream drift / loop / stuck-tool / fabrication alerts from the audit log
|
|
@@ -2814,6 +2815,12 @@ else if (command === "autostart-status") {
|
|
|
2814
2815
|
process.exit(1);
|
|
2815
2816
|
});
|
|
2816
2817
|
}
|
|
2818
|
+
else if (command === "session-gate") {
|
|
2819
|
+
import("./session-gate.js").then((m) => m.cliSessionGate(process.argv.slice(3))).catch((err) => {
|
|
2820
|
+
console.error("Error:", err);
|
|
2821
|
+
process.exit(0); // a broken gate must never trap the user in a turn
|
|
2822
|
+
});
|
|
2823
|
+
}
|
|
2817
2824
|
else if (command === "install-claude-hook") {
|
|
2818
2825
|
import("./install-claude-hook.js").then((m) => m.cliInstallClaudeHook(process.argv.slice(3))).catch((err) => {
|
|
2819
2826
|
console.error("Error:", err instanceof Error ? err.message : err);
|
|
@@ -15,12 +15,21 @@
|
|
|
15
15
|
// FIX: To add a new hook event, extend EVENT_KINDS + the splice block.
|
|
16
16
|
// Keep the "preserve existing" discipline in every code path.
|
|
17
17
|
import { existsSync, readFileSync, writeFileSync, copyFileSync, chmodSync, mkdirSync } from "fs";
|
|
18
|
-
import { join } from "path";
|
|
18
|
+
import { join, dirname } from "path";
|
|
19
19
|
import { homedir } from "os";
|
|
20
|
+
import { fileURLToPath } from "url";
|
|
21
|
+
// [LOCK] [M2-ESM-FILENAME-FIX]: the package is "type": "module", so a bare __dirname is a
|
|
22
|
+
// ReferenceError at runtime. Found 2026-09-06 while adding the Stop gate: the defaults/ lookup
|
|
23
|
+
// below used `__dirname_esm`, and a real run against a throwaway HOME died with
|
|
24
|
+
// "__dirname is not defined": the installer had never worked from the published package.
|
|
25
|
+
const __dirname_esm = dirname(fileURLToPath(import.meta.url));
|
|
20
26
|
const CLAUDE_DIR = join(homedir(), ".claude");
|
|
21
27
|
const SETTINGS_FILE = join(CLAUDE_DIR, "settings.json");
|
|
22
28
|
const HOOKS_DIR = join(CLAUDE_DIR, "hooks");
|
|
23
29
|
const HOOK_SCRIPT = join(HOOKS_DIR, "opscontext-emit.sh");
|
|
30
|
+
/** The Stop gate: a wrapper that runs `session-gate` with the node and CLI that installed it.
|
|
31
|
+
* [LOCK] [SESSION-SAVE-IS-A-GATE] (src/session-gate.ts) */
|
|
32
|
+
const GATE_SCRIPT = join(HOOKS_DIR, "opscontext-session-gate.sh");
|
|
24
33
|
const EVENT_KINDS = ["UserPromptSubmit", "PostToolUse", "SessionStart"];
|
|
25
34
|
function readSettings() {
|
|
26
35
|
if (!existsSync(SETTINGS_FILE))
|
|
@@ -52,8 +61,8 @@ function bundledHookSource() {
|
|
|
52
61
|
// when globally / locally installed via npm. Both follow the same relative
|
|
53
62
|
// shape because npm copies defaults/ via the `files` whitelist.
|
|
54
63
|
const candidates = [
|
|
55
|
-
join(
|
|
56
|
-
join(
|
|
64
|
+
join(__dirname_esm, "..", "defaults", "claude-code-hook.sh"),
|
|
65
|
+
join(__dirname_esm, "defaults", "claude-code-hook.sh"),
|
|
57
66
|
];
|
|
58
67
|
for (const c of candidates) {
|
|
59
68
|
if (existsSync(c))
|
|
@@ -73,10 +82,13 @@ Events emitted (all go through the local HTTP endpoint, never the network):
|
|
|
73
82
|
• UserPromptSubmit → vscode.prompt_submit (feeds the loop heuristic)
|
|
74
83
|
• PostToolUse → vscode.tool_call (feeds stuck + silent_failure)
|
|
75
84
|
• SessionStart → vscode.session_start
|
|
85
|
+
• Stop → the session gate: a turn cannot end while the repo's CE session
|
|
86
|
+
is older than the last commit (contextengine session-gate --help)
|
|
76
87
|
|
|
77
88
|
The installer:
|
|
78
89
|
1. Copies the bundled hook script to ~/.claude/hooks/opscontext-emit.sh
|
|
79
|
-
|
|
90
|
+
and writes ~/.claude/hooks/opscontext-session-gate.sh (node + this CLI, absolute paths)
|
|
91
|
+
2. Splices four entries into ~/.claude/settings.json under "hooks"
|
|
80
92
|
3. Preserves every existing hook entry (idempotent, safe to re-run)
|
|
81
93
|
|
|
82
94
|
A timestamped backup is written next to settings.json before any change.
|
|
@@ -130,6 +142,19 @@ Run: opscontext install-autostart
|
|
|
130
142
|
settings.hooks[kind].push(entry);
|
|
131
143
|
added++;
|
|
132
144
|
}
|
|
145
|
+
// Step 3: the Stop gate. Absolute node + CLI paths: hooks run without the user's shell PATH.
|
|
146
|
+
const cliPath = join(__dirname_esm, "cli.js");
|
|
147
|
+
writeFileSync(GATE_SCRIPT, `#!/bin/sh\n# Generated by \`opscontext install-claude-hook\`: the CE session gate on Claude Code Stop.\n# Exit 2 = the turn may not end yet (reason on stderr). See: contextengine session-gate --help\nexec "${process.execPath}" "${cliPath}" session-gate\n`);
|
|
148
|
+
chmodSync(GATE_SCRIPT, 0o755);
|
|
149
|
+
settings.hooks.Stop ??= [];
|
|
150
|
+
if (hookAlreadyWired(settings.hooks.Stop, GATE_SCRIPT)) {
|
|
151
|
+
skipped++;
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
settings.hooks.Stop.push({ hooks: [{ type: "command", command: GATE_SCRIPT, timeout: 15 }] });
|
|
155
|
+
added++;
|
|
156
|
+
}
|
|
157
|
+
console.log(`✅ Installed session gate: ${GATE_SCRIPT}`);
|
|
133
158
|
writeFileSync(SETTINGS_FILE, JSON.stringify(settings, null, 2) + "\n");
|
|
134
159
|
console.log(`✅ ${added} hook entries added, ${skipped} already present.`);
|
|
135
160
|
console.log(``);
|
|
@@ -159,11 +184,11 @@ manually if you want it gone. The audit log is NOT touched.`);
|
|
|
159
184
|
if (backup)
|
|
160
185
|
console.log(`✅ Backed up settings.json → ${backup}`);
|
|
161
186
|
let removed = 0;
|
|
162
|
-
for (const kind of EVENT_KINDS) {
|
|
187
|
+
for (const kind of [...EVENT_KINDS, "Stop"]) {
|
|
163
188
|
const entries = settings.hooks[kind];
|
|
164
189
|
if (!entries)
|
|
165
190
|
continue;
|
|
166
|
-
const filtered = entries.filter((e) => !e.hooks?.some((h) => h.command?.includes("opscontext-emit.sh")));
|
|
191
|
+
const filtered = entries.filter((e) => !e.hooks?.some((h) => h.command?.includes("opscontext-emit.sh") || h.command?.includes("opscontext-session-gate.sh")));
|
|
167
192
|
removed += entries.length - filtered.length;
|
|
168
193
|
if (filtered.length === 0) {
|
|
169
194
|
delete settings.hooks[kind];
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export interface GateInput {
|
|
2
|
+
/** The repository the turn ran in (CLAUDE_PROJECT_DIR, else cwd). */
|
|
3
|
+
repo: string;
|
|
4
|
+
/** Where CE keeps sessions; default ~/.contextengine/sessions. */
|
|
5
|
+
sessionsDir?: string;
|
|
6
|
+
/** Claude Code sets this when the turn is already continuing because of a Stop hook. */
|
|
7
|
+
stopHookActive?: boolean;
|
|
8
|
+
/** Tests: a session file to use instead of the lookup. */
|
|
9
|
+
sessionFile?: string;
|
|
10
|
+
}
|
|
11
|
+
export interface GateResult {
|
|
12
|
+
block: boolean;
|
|
13
|
+
reason: "loop_guard" | "not_git" | "no_commits" | "fresh" | "stale";
|
|
14
|
+
sessionName: string;
|
|
15
|
+
sessionTs: number;
|
|
16
|
+
commitTs: number;
|
|
17
|
+
/** Commits under src/ since copilot-instructions.md last changed; null when unknown. */
|
|
18
|
+
docsBehind: number | null;
|
|
19
|
+
/** Newest docs/sessions/SESSION_*.md, relative to the repo; null when none. */
|
|
20
|
+
sessionDoc: string | null;
|
|
21
|
+
message: string;
|
|
22
|
+
}
|
|
23
|
+
export declare function normalizeName(s: string): string;
|
|
24
|
+
/** The session for a repo: the newest file whose normalized name starts with the repo's. */
|
|
25
|
+
export declare function findRepoSession(repo: string, sessionsDir: string): {
|
|
26
|
+
name: string;
|
|
27
|
+
ts: number;
|
|
28
|
+
};
|
|
29
|
+
export declare function evaluateSessionGate(input: GateInput): GateResult;
|
|
30
|
+
/** `contextengine session-gate`: the Stop hook body. Exit 2 blocks the turn end, 0 lets it end. */
|
|
31
|
+
export declare function cliSessionGate(args: string[]): Promise<never>;
|
|
32
|
+
//# sourceMappingURL=session-gate.d.ts.map
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
// [LOCKED] [SESSION-SAVE-IS-A-GATE] 2026-09-06 (moved here from scripts/session-gate.sh, 2.7.0)
|
|
2
|
+
// [NEVER] turn this back into a reminder (exit 0 with a message), gate it on a wall clock, or
|
|
3
|
+
// make it block outside a git repo.
|
|
4
|
+
// WHY: the owner had to type "check and update session and ce docs!" at the end of every session
|
|
5
|
+
// ("why do I have to write this systematically?", 2026-09-06). The firewall nag lives inside
|
|
6
|
+
// MCP tool responses and the post-push hook only reminds; on 2026-09-05 the MCP server was
|
|
7
|
+
// disconnected for hours and the work went through the CLI, so nothing fired. A rule that
|
|
8
|
+
// lives in prose is not a rule; a Stop hook that refuses is. The first version was a shell
|
|
9
|
+
// script copied into 33 repos, which no paying user could get; now it ships in the package
|
|
10
|
+
// and `install-claude-hook` wires it once, at user scope, for every repo the user opens.
|
|
11
|
+
// FIX: compare the repo's CE session file (newest session whose normalized name starts with the
|
|
12
|
+
// repo's normalized basename) with the last commit's time. Older = block (exit 2) with the
|
|
13
|
+
// session-doc and copilot-instructions staleness in the same message, so the docs get the
|
|
14
|
+
// same pass. `stop_hook_active` from Claude Code means "you are already continuing because
|
|
15
|
+
// of me": never block twice. Not a git repo, or no commit yet: nothing to gate, pass.
|
|
16
|
+
import { existsSync, readdirSync, statSync } from "fs";
|
|
17
|
+
import { basename, join } from "path";
|
|
18
|
+
import { homedir } from "os";
|
|
19
|
+
import { execFileSync } from "child_process";
|
|
20
|
+
export function normalizeName(s) {
|
|
21
|
+
return s.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
22
|
+
}
|
|
23
|
+
function mtime(p) {
|
|
24
|
+
try {
|
|
25
|
+
return Math.floor(statSync(p).mtimeMs / 1000);
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return 0;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function git(repo, args) {
|
|
32
|
+
try {
|
|
33
|
+
// Hardcoded argv, no shell; the only variable parts are paths and a commit hash git itself printed.
|
|
34
|
+
return execFileSync("git", args, { cwd: repo, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 5000 }).trim();
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/** The session for a repo: the newest file whose normalized name starts with the repo's. */
|
|
41
|
+
export function findRepoSession(repo, sessionsDir) {
|
|
42
|
+
const name = basename(repo);
|
|
43
|
+
const want = normalizeName(name);
|
|
44
|
+
let best = { name, ts: 0 };
|
|
45
|
+
let files = [];
|
|
46
|
+
try {
|
|
47
|
+
files = readdirSync(sessionsDir);
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return best;
|
|
51
|
+
}
|
|
52
|
+
for (const f of files) {
|
|
53
|
+
if (!f.endsWith(".json"))
|
|
54
|
+
continue;
|
|
55
|
+
const base = f.slice(0, -5);
|
|
56
|
+
if (!normalizeName(base).startsWith(want))
|
|
57
|
+
continue;
|
|
58
|
+
const ts = mtime(join(sessionsDir, f));
|
|
59
|
+
if (ts > best.ts)
|
|
60
|
+
best = { name: base, ts };
|
|
61
|
+
}
|
|
62
|
+
return best;
|
|
63
|
+
}
|
|
64
|
+
function newestSessionDoc(repo) {
|
|
65
|
+
const dir = join(repo, "docs", "sessions");
|
|
66
|
+
let files = [];
|
|
67
|
+
try {
|
|
68
|
+
files = readdirSync(dir).filter((f) => /^SESSION_.*\.md$/.test(f));
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
let best = null;
|
|
74
|
+
for (const f of files) {
|
|
75
|
+
const ts = mtime(join(dir, f));
|
|
76
|
+
if (!best || ts > best.ts)
|
|
77
|
+
best = { f, ts };
|
|
78
|
+
}
|
|
79
|
+
return best ? join("docs", "sessions", best.f) : null;
|
|
80
|
+
}
|
|
81
|
+
function docsBehind(repo) {
|
|
82
|
+
const ci = ".github/copilot-instructions.md";
|
|
83
|
+
if (!existsSync(join(repo, ci)))
|
|
84
|
+
return null;
|
|
85
|
+
const last = git(repo, ["log", "-1", "--format=%H", "--", ci]);
|
|
86
|
+
if (!last)
|
|
87
|
+
return null;
|
|
88
|
+
const n = git(repo, ["rev-list", "--count", `${last}..HEAD`, "--", "src"]);
|
|
89
|
+
return n === null ? null : Number(n) || 0;
|
|
90
|
+
}
|
|
91
|
+
function fmt(ts) {
|
|
92
|
+
return ts === 0 ? "never saved" : new Date(ts * 1000).toISOString().slice(0, 16).replace("T", " ") + "Z";
|
|
93
|
+
}
|
|
94
|
+
export function evaluateSessionGate(input) {
|
|
95
|
+
const repo = input.repo;
|
|
96
|
+
const name = basename(repo);
|
|
97
|
+
const sessionsDir = input.sessionsDir ?? join(process.env.CONTEXTENGINE_HOME || join(homedir(), ".contextengine"), "sessions");
|
|
98
|
+
const base = { sessionName: name, sessionTs: 0, commitTs: 0, docsBehind: null, sessionDoc: null };
|
|
99
|
+
if (input.stopHookActive)
|
|
100
|
+
return { ...base, block: false, reason: "loop_guard", message: "" };
|
|
101
|
+
if (!git(repo, ["rev-parse", "--git-dir"]))
|
|
102
|
+
return { ...base, block: false, reason: "not_git", message: "" };
|
|
103
|
+
const commitRaw = git(repo, ["log", "-1", "--format=%ct"]);
|
|
104
|
+
const commitTs = commitRaw ? Number(commitRaw) || 0 : 0;
|
|
105
|
+
if (commitTs === 0)
|
|
106
|
+
return { ...base, block: false, reason: "no_commits", message: "" };
|
|
107
|
+
let session;
|
|
108
|
+
if (input.sessionFile)
|
|
109
|
+
session = { name: name, ts: existsSync(input.sessionFile) ? mtime(input.sessionFile) : 0 };
|
|
110
|
+
else
|
|
111
|
+
session = findRepoSession(repo, sessionsDir);
|
|
112
|
+
const doc = newestSessionDoc(repo);
|
|
113
|
+
const behind = docsBehind(repo);
|
|
114
|
+
const filled = { ...base, sessionName: session.name, sessionTs: session.ts, commitTs, docsBehind: behind, sessionDoc: doc };
|
|
115
|
+
if (session.ts >= commitTs)
|
|
116
|
+
return { ...filled, block: false, reason: "fresh", message: "" };
|
|
117
|
+
const lines = [
|
|
118
|
+
`CE session gate: session '${session.name}' (${fmt(session.ts)}) is older than the last commit (${fmt(commitTs)}). Before ending:`,
|
|
119
|
+
`1. save_session (MCP) session='${session.name}' keys summary and open, with what changed since the last save;`,
|
|
120
|
+
`2. update ${doc ?? "docs/sessions/SESSION_N.md"} if the work changed and commit it;`,
|
|
121
|
+
behind === null
|
|
122
|
+
? `3. no .github/copilot-instructions.md here; if the project has agent docs, update them the same way;`
|
|
123
|
+
: `3. .github/copilot-instructions.md is ${behind} src commit(s) behind; update it if any of them changed how the project works;`,
|
|
124
|
+
`4. save_learning for any reusable lesson. Then end the turn; this gate passes once the session file is newer than HEAD.`,
|
|
125
|
+
];
|
|
126
|
+
return { ...filled, block: true, reason: "stale", message: lines.join("\n") };
|
|
127
|
+
}
|
|
128
|
+
/** Read Claude Code's hook payload from stdin without ever blocking on a TTY. */
|
|
129
|
+
async function readStdinJson() {
|
|
130
|
+
if (process.stdin.isTTY)
|
|
131
|
+
return {};
|
|
132
|
+
return new Promise((resolve) => {
|
|
133
|
+
let buf = "";
|
|
134
|
+
const done = () => { try {
|
|
135
|
+
resolve(buf.trim() ? JSON.parse(buf) : {});
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
resolve({});
|
|
139
|
+
} };
|
|
140
|
+
process.stdin.setEncoding("utf8");
|
|
141
|
+
process.stdin.on("data", (d) => { buf += d; });
|
|
142
|
+
process.stdin.on("end", done);
|
|
143
|
+
process.stdin.on("error", done);
|
|
144
|
+
setTimeout(done, 1500).unref();
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
/** `contextengine session-gate`: the Stop hook body. Exit 2 blocks the turn end, 0 lets it end. */
|
|
148
|
+
export async function cliSessionGate(args) {
|
|
149
|
+
if (args.includes("-h") || args.includes("--help")) {
|
|
150
|
+
console.log(`Usage: contextengine session-gate (as a Claude Code Stop hook; see install-claude-hook)
|
|
151
|
+
|
|
152
|
+
Refuses to end a Claude Code turn (exit 2, reason on stderr) while the repo's CE session is
|
|
153
|
+
older than the last commit. The repo is CLAUDE_PROJECT_DIR or the cwd; the session is the newest
|
|
154
|
+
~/.contextengine/sessions/*.json whose name starts with the repo's name. Passes (exit 0) when the
|
|
155
|
+
session is newer, outside a git repo, before the first commit, or when stop_hook_active is set.`);
|
|
156
|
+
process.exit(0);
|
|
157
|
+
}
|
|
158
|
+
const payload = await readStdinJson();
|
|
159
|
+
const repo = process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
160
|
+
const r = evaluateSessionGate({ repo, stopHookActive: payload.stop_hook_active === true });
|
|
161
|
+
if (r.block) {
|
|
162
|
+
console.error(r.message);
|
|
163
|
+
process.exit(2);
|
|
164
|
+
}
|
|
165
|
+
process.exit(0);
|
|
166
|
+
}
|
|
167
|
+
//# sourceMappingURL=session-gate.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@compr/opscontext-mcp",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.7.0",
|
|
4
4
|
"description": "OpsContext for AI Agents — read-only fleet visibility (PM2/nginx/Docker/git/cron) + tamper-evident audit log + policy-as-code hooks. The ops + compliance layer Claude Code can't grow natively.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -120,6 +120,7 @@ Add to your OpenClaw `openclaw.json` MCP servers section:
|
|
|
120
120
|
| `list_projects` | Discover and analyze all projects (tech stack, git, docker, pm2) |
|
|
121
121
|
| `check_ports` | Scan all projects for port conflicts |
|
|
122
122
|
| `run_audit` | Compliance agent — git remotes, hooks, .env, Docker, PM2, versions |
|
|
123
|
+
| CLI `session-gate` | Claude Code Stop hook (installed by `install-claude-hook`): a turn cannot end while the repo's CE session is older than the last commit |
|
|
123
124
|
| `score_project` | AI-readiness scoring 0-100% with anti-gaming v2 (symlink/ghost config detection) |
|
|
124
125
|
| `save_session` | Save key-value entry to a named session for cross-session persistence |
|
|
125
126
|
| `load_session` | Load all entries from a named session |
|