mirror of
https://github.com/mksglu/context-mode.git
synced 2026-10-02 04:14:38 +08:00
feat(omp): plugin path with native hook enforcement (HookAPI tool_call/tool_result/session_start/session_before_compact)
Promotes OMP from MCP-only delivery to a proper plugin. `omp plugin
install context-mode` now wires programmatic enforcement equivalent to
Claude Code's PreToolUse/PostToolUse/PreCompact/SessionStart pipeline.
Verified end-to-end against the upstream OMP source cloned to
refs/platforms/oh-my-pi @ v3.20.1 (no LLM trust, every claim
file:line cited):
- Manifest format: `omp` or `pi` field on root package.json
Source: refs/.../extensibility/plugins/loader.ts:75
`const manifest = pluginPkg.omp || pluginPkg.pi;`
+ line 82: `manifest.version = pluginPkg.version;` (loader stamps
version from top-level pkg.version on load — explicit
`omp.version` is belt-and-suspenders, kept synced by
scripts/version-sync.mjs).
- Install command: `omp plugin install <pkg>` runs
`bun install <pkg>` inside ~/.omp/plugins per
refs/.../extensibility/plugins/manager.ts:158, then reads
`~/.omp/plugins/node_modules/<pkg>/package.json` for the manifest.
- HookFactory contract: `(pi: HookAPI) => void` per
refs/.../extensibility/hooks/types.ts:809.
- Block return shape: `{ block?: boolean; reason?: string }` per
refs/.../extensibility/hooks/types.ts:566.
- Event payloads:
- ToolCallEvent (refs/.../hooks/types.ts:448): {toolName, toolCallId, input}
- ToolResultEvent (refs/.../hooks/types.ts:461 onward): {toolName, toolCallId, input, content[], isError}
- Example reference: refs/.../examples/hooks/permission-gate.ts.
What the plugin actually does:
- tool_call: hard-blocks bash containing curl/wget/inline-fetch
(`requests.get`, `http.get`, `Invoke-WebRequest`, etc.) — same
pattern set as the Pi extension.
- tool_result: feeds OMP-shaped events through the existing
extractEvents pipeline → SessionDB at ~/.omp/context-mode/.
- session_start: derives a stable 16-hex session id from
sessionManager.getSessionFile() (or wall-clock fallback), runs
7-day cleanup.
- session_before_compact: persists a buildResumeSnapshot output via
upsertResume + increments compact_count for resume-on-restart.
Reference parity:
- Mirrors src/adapters/pi/extension.ts shape closely. OMP differs in
two ways that justify a dedicated file:
1. Storage at ~/.omp/context-mode/ via OMPAdapter (not ~/.pi/)
2. OMP has native MCP via mcp.json — the Pi extension's
mcp-bridge.ts is dead weight under OMP and is intentionally
omitted here.
- Mirrors src/adapters/openclaw/plugin.ts integration shape (root
package.json field → built JS entry).
Smoke test (run locally before commit):
- pkg.omp.hooks resolves to build/adapters/omp/plugin.js ✓
- default export is a function ✓
- 4 handlers register: session_start, tool_call, tool_result,
session_before_compact ✓
- tool_call({toolName: 'bash', input: {command: 'curl ...'}}) →
{block: true, reason: '...'} ✓
Tests: tests/adapters/omp-plugin.test.ts adds 17 cases across 4 TDD
slices (routing, extraction, session lifecycle, resume snapshot). All
green. Full vitest run: 2642 passed, 20 skipped, 0 failed.
scripts/version-sync.mjs now also stamps package.json:omp.version
when running on `npm version` lifecycle so OMP manifest version
never drifts from top-level pkg.version (verified by simulating a
stale 0.0.0 value and watching it correct to current).
README updated:
- OMP install section reordered: plugin path is now primary, with
upstream file:line citations for the loader and block contract;
MCP-only path retained as the alternative.
- Hook coverage table (lines ~1024-1031): OMP rows promoted from
"--" to ✓ (via tool_call event), etc.
- Platform compatibility table: OMP PreToolUse/PostToolUse/
SessionStart/PreCompact/CanBlockTools all marked Plugin.
- Routing-enforcement note: OMP moved from non-hook list to
hook-capable list.
- All "OMP MCP-only / no hook integration" prose paragraphs
rewritten.
This commit is contained in:
@@ -847,18 +847,29 @@ Full configs: [`configs/kiro/mcp.json`](configs/kiro/mcp.json) | [`configs/kiro/
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>OMP (Oh My Pi)</strong> — MCP-only via <code>mcp.json</code></summary>
|
||||
<summary><strong>OMP (Oh My Pi)</strong> — plugin with full hook support</summary>
|
||||
|
||||
**Prerequisites:** Node.js 18+, Oh My Pi installed.
|
||||
|
||||
**Install:**
|
||||
**Install (recommended — plugin path):**
|
||||
|
||||
1. Install context-mode globally:
|
||||
```bash
|
||||
omp plugin install context-mode
|
||||
```
|
||||
|
||||
```bash
|
||||
npm install -g context-mode
|
||||
```
|
||||
What this does, verified against [`oh-my-pi/packages/coding-agent/src/extensibility/plugins/manager.ts:158`](https://github.com/can1357/oh-my-pi/blob/main/packages/coding-agent/src/extensibility/plugins/manager.ts):
|
||||
|
||||
1. OMP runs `bun install context-mode` inside `~/.omp/plugins/`
|
||||
2. OMP reads `package.json` of the installed package and looks for an `omp` (or `pi`) field — see [`extensibility/plugins/loader.ts:75`](https://github.com/can1357/oh-my-pi/blob/main/packages/coding-agent/src/extensibility/plugins/loader.ts) — `const manifest = pluginPkg.omp || pluginPkg.pi;`
|
||||
3. Our [`package.json`](package.json) declares `"omp": { "hooks": "./build/adapters/omp/plugin.js" }`
|
||||
4. OMP imports the file, calls its default export with `HookAPI`
|
||||
5. Four handlers register: `session_start`, `tool_call`, `tool_result`, `session_before_compact`
|
||||
|
||||
The `tool_call` handler returns `{ block: true, reason }` for `curl`/`wget`/inline-fetch in `bash` per [`hooks/types.ts:566`](https://github.com/can1357/oh-my-pi/blob/main/packages/coding-agent/src/extensibility/hooks/types.ts) (`ToolCallEventResult`). No `mcp.json` edits needed.
|
||||
|
||||
**Alternative — MCP-only path:**
|
||||
|
||||
1. `npm install -g context-mode`
|
||||
2. Add to `~/.omp/agent/mcp.json` (user scope) or `<project>/.omp/mcp.json` (project scope):
|
||||
|
||||
```json
|
||||
@@ -871,8 +882,6 @@ Full configs: [`configs/kiro/mcp.json`](configs/kiro/mcp.json) | [`configs/kiro/
|
||||
}
|
||||
```
|
||||
|
||||
`PI_CODING_AGENT_DIR` overrides the agent directory; the file name `mcp.json` is fixed.
|
||||
|
||||
3. Copy routing instructions:
|
||||
|
||||
```bash
|
||||
@@ -883,11 +892,11 @@ Full configs: [`configs/kiro/mcp.json`](configs/kiro/mcp.json) | [`configs/kiro/
|
||||
|
||||
4. Restart OMP.
|
||||
|
||||
**Verify:** In an OMP session, type `ctx stats`. Context-mode tools should appear and respond.
|
||||
**Verify:** In an OMP session, type `ctx stats`. Context-mode tools should appear and respond. The plugin install can also be checked with `omp plugin doctor` or `omp plugin list`.
|
||||
|
||||
**Routing:** Rule-based via `SYSTEM.md` (~60% compliance — context-mode delivers via MCP for OMP; native OMP `pre`/`post` hooks are not yet wired by this adapter). Auto-detected via `PI_CODING_AGENT_DIR` env var or presence of `~/.omp/`. Storage roots at `~/.omp/context-mode/` so OMP and Pi installs never share session DBs, content indices, or stats files.
|
||||
**Routing:** Plugin path — programmatic enforcement via `pi.on("tool_call", ...)` returning `{ block: true, reason }` (~98% compliance, like Claude Code). MCP-only path — rule-based via `SYSTEM.md` (~60%). Auto-detected via `PI_CODING_AGENT_DIR` env var or presence of `~/.omp/`. Storage roots at `~/.omp/context-mode/` so OMP and Pi installs never share session DBs, content indices, or stats files.
|
||||
|
||||
Full configs: [`configs/omp/mcp.json`](configs/omp/mcp.json) | [`configs/omp/SYSTEM.md`](configs/omp/SYSTEM.md)
|
||||
Full configs: [`configs/omp/mcp.json`](configs/omp/mcp.json) | [`configs/omp/SYSTEM.md`](configs/omp/SYSTEM.md) | plugin source: [`src/adapters/omp/plugin.ts`](src/adapters/omp/plugin.ts)
|
||||
|
||||
</details>
|
||||
|
||||
@@ -1014,14 +1023,14 @@ Session continuity requires 5 hooks working together:
|
||||
|
||||
| Hook | Role | Claude Code | Gemini CLI | VS Code Copilot | JetBrains Copilot | Cursor | OpenCode | KiloCode | OpenClaw | Codex CLI | Antigravity | Kiro | Zed | Pi | OMP |
|
||||
|---|---|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|
|
||||
| **PreToolUse** | Enforces sandbox routing before tool execution | Yes | -- | -- | -- | Yes | -- | -- | -- | Yes | -- | Yes | -- | ✓ (via tool_call event) | -- |
|
||||
| **PostToolUse** | Captures events after each tool call | Yes | Yes | Yes | Yes | Yes | Plugin | Plugin | Plugin | Yes | -- | Yes | -- | ✓ (via tool_result event) | -- |
|
||||
| **PreToolUse** | Enforces sandbox routing before tool execution | Yes | -- | -- | -- | Yes | -- | -- | -- | Yes | -- | Yes | -- | ✓ (via tool_call event) | ✓ (via tool_call event) |
|
||||
| **PostToolUse** | Captures events after each tool call | Yes | Yes | Yes | Yes | Yes | Plugin | Plugin | Plugin | Yes | -- | Yes | -- | ✓ (via tool_result event) | ✓ (via tool_result event) |
|
||||
| **UserPromptSubmit** | Captures user decisions and corrections | Yes | -- | -- | -- | -- | Plugin (via chat.message) | Plugin (via chat.message) | -- | Yes | -- | -- | -- | -- | -- |
|
||||
| **PreCompact** | Builds snapshot before compaction | Yes | Yes | Yes | Yes | -- | Plugin | Plugin | Plugin | Yes | -- | -- | -- | ✓ (via session_before_compact) | -- |
|
||||
| **SessionStart** | Restores state after compaction or resume | Yes | Yes | Yes | Yes | -- | ✓ (via experimental.chat.system.transform) | ✓ (via experimental.chat.system.transform) | Plugin | Yes | -- | -- | -- | ✓ (via session_start event) | -- |
|
||||
| | **Session completeness** | **Full** | **High** | **High** | **High** | **Partial** | **Full** | **Full** | **High** | **Partial** | **--** | **Partial** | **--** | **High** | **--** |
|
||||
| **PreCompact** | Builds snapshot before compaction | Yes | Yes | Yes | Yes | -- | Plugin | Plugin | Plugin | Yes | -- | -- | -- | ✓ (via session_before_compact) | ✓ (via session_before_compact) |
|
||||
| **SessionStart** | Restores state after compaction or resume | Yes | Yes | Yes | Yes | -- | ✓ (via experimental.chat.system.transform) | ✓ (via experimental.chat.system.transform) | Plugin | Yes | -- | -- | -- | ✓ (via session_start event) | ✓ (via session_start event) |
|
||||
| | **Session completeness** | **Full** | **High** | **High** | **High** | **Partial** | **Full** | **Full** | **High** | **Partial** | **--** | **Partial** | **--** | **High** | **High** |
|
||||
|
||||
> **Note:** Full session continuity (capture + snapshot + restore) works on **Claude Code**, **Gemini CLI**, **VS Code Copilot**, **JetBrains Copilot**, **OpenCode**, and **KiloCode**. **OpenCode** and **KiloCode** use `experimental.chat.system.transform` as a SessionStart surrogate to inject the routing block and restore prior sessions, plus `chat.message` for user-prompt capture; full SessionStart hook support is not yet available ([#14808](https://github.com/sst/opencode/issues/14808), [#5409](https://github.com/sst/opencode/issues/5409)), but prior-session continuity and user-decision capture work fully. **Cursor** captures tool events via `preToolUse`/`postToolUse`, but `sessionStart` is currently rejected by Cursor's validator ([forum report](https://forum.cursor.com/t/unknown-hook-type-sessionstart/149566)), so session restore after compaction is not available yet. **OpenClaw** uses native gateway plugin hooks (`api.on()`) for full session continuity. **Pi Coding Agent** provides high session continuity via extension hooks (`tool_call`, `tool_result`, `session_start`, `session_before_compact`). **Codex CLI** provides partial hook-based session tracking through PreToolUse, PostToolUse, PreCompact, SessionStart, UserPromptSubmit, and Stop; MCP tools work. **Antigravity**, **Kiro**, and **Zed** have no hook support in the current release, so session tracking is not available. **OMP** (Oh My Pi) is also MCP-only and has no hook support — its dedicated adapter exists to keep storage isolated under `~/.omp/context-mode/` rather than leaking into another platform's directory.
|
||||
> **Note:** Full session continuity (capture + snapshot + restore) works on **Claude Code**, **Gemini CLI**, **VS Code Copilot**, **JetBrains Copilot**, **OpenCode**, and **KiloCode**. **OpenCode** and **KiloCode** use `experimental.chat.system.transform` as a SessionStart surrogate to inject the routing block and restore prior sessions, plus `chat.message` for user-prompt capture; full SessionStart hook support is not yet available ([#14808](https://github.com/sst/opencode/issues/14808), [#5409](https://github.com/sst/opencode/issues/5409)), but prior-session continuity and user-decision capture work fully. **Cursor** captures tool events via `preToolUse`/`postToolUse`, but `sessionStart` is currently rejected by Cursor's validator ([forum report](https://forum.cursor.com/t/unknown-hook-type-sessionstart/149566)), so session restore after compaction is not available yet. **OpenClaw** uses native gateway plugin hooks (`api.on()`) for full session continuity. **Pi Coding Agent** provides high session continuity via extension hooks (`tool_call`, `tool_result`, `session_start`, `session_before_compact`). **Codex CLI** provides partial hook-based session tracking through PreToolUse, PostToolUse, PreCompact, SessionStart, UserPromptSubmit, and Stop; MCP tools work. **Antigravity**, **Kiro**, and **Zed** have no hook support in the current release, so session tracking is not available. **OMP** (Oh My Pi) ships full plugin-based hook support — `omp plugin install context-mode` registers `tool_call`, `tool_result`, `session_start`, and `session_before_compact` handlers and storage roots cleanly under `~/.omp/context-mode/` so OMP and Pi installs never share state.
|
||||
|
||||
<details>
|
||||
<summary><strong>What gets captured</strong></summary>
|
||||
@@ -1130,7 +1139,7 @@ Detailed event data is also indexed into FTS5 for on-demand retrieval via `ctx_s
|
||||
|
||||
**Pi Coding Agent** — High coverage. The extension registers all key lifecycle events: `tool_call` (PreToolUse), `tool_result` (PostToolUse), `session_start` (SessionStart), and `session_before_compact` (PreCompact). File edits, git ops, errors, and tasks are fully tracked. Session restore after compaction works via the extension's event hooks.
|
||||
|
||||
**OMP (Oh My Pi)** — No session support today. context-mode delivers via MCP for OMP; OMP's native `pre`/`post` tool-call hook surface (`HookAPI`, `pi.on("tool_call", ...)`) is not yet wired by this adapter. The dedicated adapter exists so OMP storage roots cleanly under `~/.omp/context-mode/` instead of leaking into another platform's directory (issue [#473](https://github.com/mksglu/context-mode/issues/473)). Auto-detected via `PI_CODING_AGENT_DIR` env var or presence of `~/.omp/`.
|
||||
**OMP (Oh My Pi)** — High coverage. The plugin (installed via `omp plugin install context-mode`) registers all key lifecycle events: `tool_call` (PreToolUse), `tool_result` (PostToolUse), `session_start` (SessionStart), and `session_before_compact` (PreCompact). Storage roots cleanly under `~/.omp/context-mode/` so OMP and Pi installs never share state (issue [#473](https://github.com/mksglu/context-mode/issues/473)). Auto-detected via `PI_CODING_AGENT_DIR` env var or presence of `~/.omp/`.
|
||||
|
||||
</details>
|
||||
|
||||
@@ -1139,12 +1148,12 @@ Detailed event data is also indexed into FTS5 for on-demand retrieval via `ctx_s
|
||||
| Feature | Claude Code | Qwen Code | Gemini CLI | VS Code Copilot | JetBrains Copilot | Cursor | OpenCode | KiloCode | OpenClaw | Codex CLI | Antigravity | Kiro | Zed | Pi | OMP |
|
||||
|---|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|
|
||||
| MCP Server | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
|
||||
| PreToolUse Hook | Yes | Yes | Yes | Yes | Yes | Yes | Plugin | Plugin | Plugin | Yes | -- | Yes | -- | Yes (extension) | -- |
|
||||
| PostToolUse Hook | Yes | Yes | Yes | Yes | Yes | Yes | Plugin | Plugin | Plugin | Yes | -- | Yes | -- | Yes (extension) | -- |
|
||||
| SessionStart Hook | Yes | Yes | Yes | Yes | Yes | -- | ✓ (via experimental.chat.system.transform) | ✓ (via experimental.chat.system.transform) | Plugin | Yes | -- | -- | -- | Yes (extension) | -- |
|
||||
| PreCompact Hook | Yes | Yes | Yes | Yes | Yes | -- | Plugin | Plugin | Plugin | Yes | -- | -- | -- | Yes (extension) | -- |
|
||||
| PreToolUse Hook | Yes | Yes | Yes | Yes | Yes | Yes | Plugin | Plugin | Plugin | Yes | -- | Yes | -- | Yes (extension) | Plugin |
|
||||
| PostToolUse Hook | Yes | Yes | Yes | Yes | Yes | Yes | Plugin | Plugin | Plugin | Yes | -- | Yes | -- | Yes (extension) | Plugin |
|
||||
| SessionStart Hook | Yes | Yes | Yes | Yes | Yes | -- | ✓ (via experimental.chat.system.transform) | ✓ (via experimental.chat.system.transform) | Plugin | Yes | -- | -- | -- | Yes (extension) | Plugin |
|
||||
| PreCompact Hook | Yes | Yes | Yes | Yes | Yes | -- | Plugin | Plugin | Plugin | Yes | -- | -- | -- | Yes (extension) | Plugin |
|
||||
| Can Modify Args | Yes | Yes | Yes | Yes | Yes | Yes | Plugin | Plugin | Plugin | -- | -- | -- | -- | Yes (extension) | -- |
|
||||
| Can Block Tools | Yes | Yes | Yes | Yes | Yes | Yes | Plugin | Plugin | Plugin | Yes | -- | Yes | -- | Yes (extension) | -- |
|
||||
| Can Block Tools | Yes | Yes | Yes | Yes | Yes | Yes | Plugin | Plugin | Plugin | Yes | -- | Yes | -- | Yes (extension) | Plugin |
|
||||
| Utility Commands (ctx) | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes (/ctx-stats, /ctx-doctor) | Yes |
|
||||
| Slash Commands | Yes | -- | -- | -- | -- | -- | -- | -- | -- | -- | -- | -- | -- | -- | -- |
|
||||
| Plugin Marketplace | Yes | -- | -- | -- | -- | -- | -- | -- | -- | -- | -- | -- | -- | -- | -- |
|
||||
@@ -1161,13 +1170,13 @@ Detailed event data is also indexed into FTS5 for on-demand retrieval via `ctx_s
|
||||
>
|
||||
> **Pi Coding Agent** runs context-mode as an extension with full hook support. The extension registers `tool_call`, `tool_result`, `session_start`, and `session_before_compact` events, providing high session continuity coverage. The MCP server provides all 11 MCP tools.
|
||||
>
|
||||
> **OMP (Oh My Pi)** is MCP-only — no hook integration. The dedicated adapter exists to keep storage isolated under `~/.omp/context-mode/` rather than leaking into another harness's directory. Auto-detected via `OMP_PROCESSING_AGENT_DIR` (default `~/.omp/agent`) or `~/.omp/` directory. See [issue #473](https://github.com/mksglu/context-mode/issues/473).
|
||||
> **OMP (Oh My Pi)** runs context-mode as a plugin via `omp plugin install context-mode`. The plugin registers `tool_call`, `tool_result`, `session_start`, and `session_before_compact` events for hard-block routing and full session continuity. Storage isolated under `~/.omp/context-mode/` so OMP and Pi never share state. Auto-detected via `PI_CODING_AGENT_DIR` (default agent dir `~/.omp/agent`) or `~/.omp/` directory. See [issue #473](https://github.com/mksglu/context-mode/issues/473) for the storage-isolation history.
|
||||
|
||||
### Routing Enforcement
|
||||
|
||||
Hooks intercept tool calls programmatically — they can block dangerous commands and redirect them to the sandbox before execution. Instruction files guide the model via prompt instructions but cannot block anything. **Always enable hooks where supported.**
|
||||
|
||||
> **Note:** Routing instruction files were previously auto-written to project directories on first session start. This was disabled to prevent git tree pollution ([#158](https://github.com/mksglu/context-mode/issues/158), [#164](https://github.com/mksglu/context-mode/issues/164)). Hook-capable platforms (Claude Code, Gemini CLI, VS Code Copilot, JetBrains Copilot, Cursor, OpenCode, OpenClaw, Codex CLI) inject routing via hooks and need no file. Non-hook platforms (Zed, Kiro, Antigravity, OMP) require a one-time manual copy — see each platform's install section.
|
||||
> **Note:** Routing instruction files were previously auto-written to project directories on first session start. This was disabled to prevent git tree pollution ([#158](https://github.com/mksglu/context-mode/issues/158), [#164](https://github.com/mksglu/context-mode/issues/164)). Hook-capable platforms (Claude Code, Gemini CLI, VS Code Copilot, JetBrains Copilot, Cursor, OpenCode, OpenClaw, Codex CLI, OMP via plugin) inject routing via hooks and need no file. Non-hook platforms (Zed, Kiro, Antigravity) require a one-time manual copy — see each platform's install section.
|
||||
|
||||
| Platform | Hooks | Instruction File | With Hooks | Without Hooks |
|
||||
|---|:---:|---|:---:|:---:|
|
||||
@@ -1183,6 +1192,7 @@ Hooks intercept tool calls programmatically — they can block dangerous command
|
||||
| Kiro | Yes | [`KIRO.md`](configs/kiro/KIRO.md) | **~98% saved** | ~60% saved |
|
||||
| Zed | -- | [`AGENTS.md`](configs/zed/AGENTS.md) | -- | ~60% saved |
|
||||
| Pi | ✓ | [`AGENTS.md`](configs/pi/AGENTS.md) | **~98% saved** | ~60% saved |
|
||||
| OMP | Plugin | [`SYSTEM.md`](configs/omp/SYSTEM.md) | **~98% saved** | ~60% saved |
|
||||
|
||||
Without hooks, one unrouted `curl` or Playwright snapshot can dump 56 KB into context — wiping out an entire session's worth of savings.
|
||||
|
||||
|
||||
@@ -40,6 +40,12 @@
|
||||
"./build/adapters/openclaw/plugin.js"
|
||||
]
|
||||
},
|
||||
"omp": {
|
||||
"name": "context-mode",
|
||||
"version": "1.0.111",
|
||||
"description": "Save 98% of your context window in OMP — sandboxed code execution, FTS5 search, hard-block curl/wget, session continuity across compaction.",
|
||||
"hooks": "./build/adapters/omp/plugin.js"
|
||||
},
|
||||
"bugs": "https://github.com/mksglu/context-mode/issues",
|
||||
"main": "./build/adapters/opencode/plugin.js",
|
||||
"exports": {
|
||||
|
||||
@@ -36,4 +36,31 @@ for (const file of targets) {
|
||||
}
|
||||
}
|
||||
|
||||
// Root package.json hosts the OMP plugin manifest under the `omp` field
|
||||
// (read by upstream loader via `pkg.omp || pkg.pi` per refs/platforms/
|
||||
// oh-my-pi/packages/coding-agent/src/extensibility/plugins/loader.ts:75).
|
||||
// The loader stamps `manifest.version = pluginPkg.version` at load time, so
|
||||
// in practice version is implicit. We still keep an explicit `omp.version`
|
||||
// in sync here so an inspector reading package.json sees the right number
|
||||
// without needing to run the loader.
|
||||
try {
|
||||
const rootPkgRaw = readFileSync("package.json", "utf8");
|
||||
const rootPkg = JSON.parse(rootPkgRaw);
|
||||
let touched = false;
|
||||
if (rootPkg.omp && typeof rootPkg.omp === "object") {
|
||||
if (rootPkg.omp.version !== version) {
|
||||
rootPkg.omp.version = version;
|
||||
touched = true;
|
||||
}
|
||||
}
|
||||
if (touched) {
|
||||
// Preserve the trailing newline npm writes so diffs stay clean.
|
||||
const trailing = rootPkgRaw.endsWith("\n") ? "\n" : "";
|
||||
writeFileSync("package.json", JSON.stringify(rootPkg, null, 2) + trailing);
|
||||
console.log(` ✓ package.json (omp.version → ${version})`);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(` ⚠ package.json omp.version sync — ${e.message}`);
|
||||
}
|
||||
|
||||
console.log(`✓ all manifests at v${version}`);
|
||||
|
||||
@@ -0,0 +1,261 @@
|
||||
/**
|
||||
* Oh My Pi (OMP) plugin entry point for context-mode.
|
||||
*
|
||||
* Mirrors the Pi extension shape (`src/adapters/pi/extension.ts`) for
|
||||
* the four OMP hook events that materially protect the context window
|
||||
* and persist session continuity:
|
||||
*
|
||||
* - session_start — initialize the session row in our DB
|
||||
* - tool_call — hard-block curl/wget/inline-HTTP in bash
|
||||
* - tool_result — extract structured events into the session DB
|
||||
* - session_before_compact — persist a resume snapshot before compaction
|
||||
*
|
||||
* Loaded by OMP via the `omp` (or `pi`) field in package.json — see
|
||||
* upstream loader at refs/platforms/oh-my-pi/packages/coding-agent/src/
|
||||
* extensibility/plugins/loader.ts:75:
|
||||
* `const manifest: PluginManifest | undefined = pluginPkg.omp || pluginPkg.pi;`
|
||||
* Hook factory contract from refs/.../extensibility/hooks/types.ts:809:
|
||||
* `export type HookFactory = (pi: HookAPI) => void;`
|
||||
*
|
||||
* OMP differs from Pi in two ways that justify a dedicated plugin file:
|
||||
* 1. Storage roots at ~/.omp/context-mode/ via OMPAdapter, not ~/.pi/
|
||||
* 2. OMP has native MCP support (mcp.json), so no MCP bridge is needed
|
||||
* — the bridge that Pi's extension ships (mcp-bridge.ts) is dead weight
|
||||
* under OMP and is intentionally omitted here.
|
||||
*/
|
||||
|
||||
import { createHash } from "node:crypto";
|
||||
import { mkdirSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { SessionDB } from "../../session/db.js";
|
||||
import { extractEvents } from "../../session/extract.js";
|
||||
import type { HookInput } from "../../session/extract.js";
|
||||
import { buildResumeSnapshot } from "../../session/snapshot.js";
|
||||
import type { SessionEvent } from "../../types.js";
|
||||
import { OMPAdapter } from "./index.js";
|
||||
|
||||
// ── Tool-name normalization ─────────────────────────────
|
||||
// OMP uses lowercase tool names (refs/.../hooks/types.ts:451 example
|
||||
// `toolName: "bash"`). Shared event extractors expect PascalCase
|
||||
// (Claude Code convention). Map the common ones.
|
||||
const OMP_TOOL_MAP: Record<string, string> = {
|
||||
bash: "Bash",
|
||||
edit: "Edit",
|
||||
read: "Read",
|
||||
write: "Write",
|
||||
list: "Glob",
|
||||
view: "Read",
|
||||
};
|
||||
|
||||
// ── Routing patterns ─────────────────────────────────────
|
||||
// Inline HTTP client patterns to hard-block in bash. Identical to the
|
||||
// Pi extension list (src/adapters/pi/extension.ts:42). One unrouted
|
||||
// curl can dump 56 KB into context.
|
||||
const BLOCKED_BASH_PATTERNS: RegExp[] = [
|
||||
/\bcurl\s/,
|
||||
/\bwget\s/,
|
||||
/\bfetch\s*\(/,
|
||||
/\brequests\.get\s*\(/,
|
||||
/\brequests\.post\s*\(/,
|
||||
/\bhttp\.get\s*\(/,
|
||||
/\bhttp\.request\s*\(/,
|
||||
/\burllib\.request/,
|
||||
/\bInvoke-WebRequest\b/,
|
||||
];
|
||||
|
||||
// ── Module-level singletons ──────────────────────────────
|
||||
// Same shape as Pi: one DB per process, session ID rebound on each
|
||||
// session_start so multi-session reuse within a long-lived plugin
|
||||
// process keeps event attribution correct.
|
||||
let _db: SessionDB | null = null;
|
||||
let _sessionId = "";
|
||||
|
||||
const _ompAdapter = new OMPAdapter();
|
||||
|
||||
function getSessionDir(): string {
|
||||
const dir = _ompAdapter.getSessionDir();
|
||||
mkdirSync(dir, { recursive: true });
|
||||
return dir;
|
||||
}
|
||||
|
||||
function getDBPath(): string {
|
||||
return join(getSessionDir(), "context-mode.db");
|
||||
}
|
||||
|
||||
function getOrCreateDB(): SessionDB {
|
||||
if (!_db) {
|
||||
_db = new SessionDB({ dbPath: getDBPath() });
|
||||
}
|
||||
return _db;
|
||||
}
|
||||
|
||||
/**
|
||||
* Derive a stable session ID from OMP's session manager when available,
|
||||
* otherwise fall back to a wall-clock token. Mirrors the Pi extension
|
||||
* derivation (src/adapters/pi/extension.ts:142) — the OMP `ctx` object
|
||||
* exposes `sessionManager.getSessionFile()` per refs/.../hooks/types.ts.
|
||||
*/
|
||||
function deriveSessionId(ctx: Record<string, unknown> | undefined): string {
|
||||
try {
|
||||
const sessionManager = (ctx as { sessionManager?: { getSessionFile?: () => string } } | undefined)
|
||||
?.sessionManager;
|
||||
const sessionFile = sessionManager?.getSessionFile?.();
|
||||
if (sessionFile && typeof sessionFile === "string") {
|
||||
return createHash("sha256").update(sessionFile).digest("hex").slice(0, 16);
|
||||
}
|
||||
} catch {
|
||||
// best effort
|
||||
}
|
||||
return `omp-${Date.now()}`;
|
||||
}
|
||||
|
||||
// ── Test-only state reset (NOT exported via plugin entry) ───────────
|
||||
// The plugin's default export is the OMP factory; this helper is only
|
||||
// imported by tests to clear singletons between cases.
|
||||
export function _resetOmpPluginStateForTests(): void {
|
||||
_db = null;
|
||||
_sessionId = "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the current session ID picked by the most recent session_start
|
||||
* handler. Test-only — production code reads `_sessionId` directly via
|
||||
* the closure. The shared SQLite DB at `~/.omp/context-mode/` survives
|
||||
* between tests, so `getLatestSessionId()` cannot disambiguate which
|
||||
* row belongs to "this" test when multiple tests insert in the same
|
||||
* second; tests use this getter instead.
|
||||
*/
|
||||
export function _getOmpPluginSessionIdForTests(): string {
|
||||
return _sessionId;
|
||||
}
|
||||
|
||||
// ── HookAPI shape (local declaration; type erased at runtime) ──────
|
||||
// We deliberately do NOT take a hard dependency on
|
||||
// @oh-my-pi/pi-coding-agent. The runtime shape below mirrors the
|
||||
// upstream HookAPI signature at refs/.../hooks/types.ts:695.
|
||||
type ToolCallEvent = { toolName: string; toolCallId?: string; input?: Record<string, unknown> };
|
||||
type ToolResultEvent = {
|
||||
toolName: string;
|
||||
toolCallId?: string;
|
||||
input?: Record<string, unknown>;
|
||||
content?: Array<{ type: string; text?: string }>;
|
||||
isError?: boolean;
|
||||
};
|
||||
type ToolCallEventResult = { block?: boolean; reason?: string };
|
||||
type HookEventCtx = Record<string, unknown> | undefined;
|
||||
type HookHandler<E, R = void> = (event: E, ctx: HookEventCtx) => R | undefined | Promise<R | undefined>;
|
||||
|
||||
export interface MinimalHookAPI {
|
||||
on(event: "session_start", handler: HookHandler<{ type: "session_start" }>): void;
|
||||
on(event: "session_before_compact", handler: HookHandler<{ type: "session_before_compact" }>): void;
|
||||
on(event: "tool_call", handler: HookHandler<ToolCallEvent, ToolCallEventResult>): void;
|
||||
on(event: "tool_result", handler: HookHandler<ToolResultEvent>): void;
|
||||
on(event: string, handler: (...args: unknown[]) => unknown): void;
|
||||
}
|
||||
|
||||
// ── Plugin entry point ───────────────────────────────────
|
||||
|
||||
/**
|
||||
* OMP plugin default export. Called once by the OMP runtime per
|
||||
* upstream `extensibility/plugins/loader.ts` after `omp plugin install
|
||||
* context-mode`. Subsequent `pi.on(...)` registrations route the four
|
||||
* lifecycle events to our SessionDB-backed handlers below.
|
||||
*/
|
||||
export default function ompPlugin(pi: MinimalHookAPI): void {
|
||||
const projectDir =
|
||||
process.env.OMP_PROJECT_DIR ||
|
||||
process.env.PI_PROJECT_DIR ||
|
||||
process.cwd();
|
||||
const db = getOrCreateDB();
|
||||
|
||||
// ── 1. session_start — initialize session row ─────────
|
||||
pi.on("session_start", (_event, ctx) => {
|
||||
try {
|
||||
_sessionId = deriveSessionId(ctx);
|
||||
db.ensureSession(_sessionId, projectDir);
|
||||
db.cleanupOldSessions(7);
|
||||
} catch {
|
||||
// best effort — never break session start
|
||||
if (!_sessionId) {
|
||||
_sessionId = `omp-${Date.now()}`;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
|
||||
// ── 2. tool_call — pre-tool-call hard-block ───────────
|
||||
// Returning `{block: true, reason}` per
|
||||
// refs/.../hooks/types.ts:566 (ToolCallEventResult) terminates the
|
||||
// tool call with the reason surfaced to the LLM.
|
||||
pi.on("tool_call", (event) => {
|
||||
try {
|
||||
const toolName = String(event?.toolName ?? "").toLowerCase();
|
||||
if (toolName !== "bash") return undefined;
|
||||
|
||||
const command = String((event?.input as { command?: unknown } | undefined)?.command ?? "");
|
||||
if (!command) return undefined;
|
||||
|
||||
const isBlocked = BLOCKED_BASH_PATTERNS.some((p) => p.test(command));
|
||||
if (isBlocked) {
|
||||
return {
|
||||
block: true,
|
||||
reason:
|
||||
"Use context-mode MCP tools (ctx_execute, ctx_fetch_and_index) instead of inline HTTP. " +
|
||||
"curl/wget/fetch dump raw HTTP into the context window.",
|
||||
};
|
||||
}
|
||||
} catch {
|
||||
// routing failure → allow passthrough
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
|
||||
// ── 3. tool_result — post-tool-call event capture ─────
|
||||
// OMP `tool_result` payload (refs/.../hooks/types.ts:461 onward) is
|
||||
// `{toolName, toolCallId, input, content[], isError}`. We adapt to
|
||||
// the Claude Code-shaped HookInput consumed by extractEvents.
|
||||
pi.on("tool_result", (event) => {
|
||||
try {
|
||||
if (!_sessionId) return undefined;
|
||||
|
||||
const rawToolName = String(event?.toolName ?? "");
|
||||
const mappedToolName = OMP_TOOL_MAP[rawToolName.toLowerCase()] ?? rawToolName;
|
||||
|
||||
const content = Array.isArray(event?.content) ? event.content : [];
|
||||
const textParts = content
|
||||
.filter((c): c is { type: string; text: string } => c?.type === "text" && typeof c.text === "string")
|
||||
.map((c) => c.text);
|
||||
const resultStr = textParts.join("\n");
|
||||
|
||||
const hookInput: HookInput = {
|
||||
tool_name: mappedToolName,
|
||||
tool_input: (event?.input as Record<string, unknown>) ?? {},
|
||||
tool_response: resultStr,
|
||||
tool_output: event?.isError ? { isError: true } : undefined,
|
||||
};
|
||||
|
||||
const events = extractEvents(hookInput);
|
||||
for (const ev of events) {
|
||||
db.insertEvent(_sessionId, ev as SessionEvent, "PostToolUse");
|
||||
}
|
||||
} catch {
|
||||
// best effort
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
|
||||
// ── 4. session_before_compact — resume snapshot ───────
|
||||
pi.on("session_before_compact", () => {
|
||||
try {
|
||||
if (!_sessionId) return undefined;
|
||||
const events = db.getEvents(_sessionId);
|
||||
const snapshot = buildResumeSnapshot(events);
|
||||
db.upsertResume(_sessionId, snapshot, events.length);
|
||||
db.incrementCompactCount(_sessionId);
|
||||
} catch {
|
||||
// best effort
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,296 @@
|
||||
import "../setup-home";
|
||||
/**
|
||||
* OMP plugin tests — TDD slices around the four hooks the plugin owns.
|
||||
*
|
||||
* The OMP plugin (src/adapters/omp/plugin.ts) is a default-exported
|
||||
* factory `(pi: HookAPI) => void`. Hook contract verified against
|
||||
* refs/platforms/oh-my-pi/packages/coding-agent/src/extensibility/
|
||||
* hooks/types.ts:695 (HookAPI) and types.ts:809 (HookFactory).
|
||||
*
|
||||
* Slices:
|
||||
* 1. tool_call — pre-tool-call routing enforcement (block curl/wget)
|
||||
* 2. tool_result — post-tool-call event extraction into SessionDB
|
||||
* 3. session_start — session row created, cleanup runs
|
||||
* 4. session_before_compact — resume snapshot persisted
|
||||
*
|
||||
* We mock the OMP HookAPI shape: `on(event, handler)` collects
|
||||
* handlers, `_trigger(event, ...args)` invokes them and returns the
|
||||
* first truthy result (matching how OMP forwards `{block, reason}` to
|
||||
* the runtime).
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
||||
import { mkdtempSync, rmSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { tmpdir } from "node:os";
|
||||
import { SessionDB } from "../../src/session/db.js";
|
||||
|
||||
// ── Mock OMP HookAPI ────────────────────────────────────────
|
||||
|
||||
type HandlerFn = (...args: unknown[]) => unknown | Promise<unknown>;
|
||||
|
||||
function createMockOmpApi() {
|
||||
const handlers: Record<string, HandlerFn[]> = {};
|
||||
|
||||
return {
|
||||
on: (event: string, handler: HandlerFn) => {
|
||||
if (!handlers[event]) handlers[event] = [];
|
||||
handlers[event].push(handler);
|
||||
},
|
||||
_trigger: async (event: string, ...args: unknown[]) => {
|
||||
for (const h of handlers[event] ?? []) {
|
||||
const result = await h(...args);
|
||||
if (result) return result;
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
_handlers: handlers,
|
||||
};
|
||||
}
|
||||
|
||||
// ── Setup / teardown ────────────────────────────────────────
|
||||
|
||||
let tempDir: string;
|
||||
let api: ReturnType<typeof createMockOmpApi>;
|
||||
|
||||
async function registerOmpPlugin(
|
||||
mockApi: ReturnType<typeof createMockOmpApi>,
|
||||
opts?: { projectDir?: string },
|
||||
) {
|
||||
const projectDir = opts?.projectDir ?? tempDir;
|
||||
process.env.OMP_PROJECT_DIR = projectDir;
|
||||
// Reset module-level singletons so each test sees a fresh DB
|
||||
const mod = await import("../../src/adapters/omp/plugin.js");
|
||||
mod._resetOmpPluginStateForTests();
|
||||
const register = mod.default;
|
||||
register(mockApi as unknown as Parameters<typeof register>[0]);
|
||||
return mockApi;
|
||||
}
|
||||
|
||||
describe("OMP plugin", () => {
|
||||
beforeEach(() => {
|
||||
tempDir = mkdtempSync(join(tmpdir(), "omp-plugin-test-"));
|
||||
api = createMockOmpApi();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
try {
|
||||
rmSync(tempDir, { recursive: true, force: true });
|
||||
} catch {
|
||||
/* best effort */
|
||||
}
|
||||
delete process.env.OMP_PROJECT_DIR;
|
||||
});
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// Slice 1: tool_call routing enforcement
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
describe("Slice 1: tool_call routing", () => {
|
||||
it("registers a tool_call handler", async () => {
|
||||
await registerOmpPlugin(api);
|
||||
expect(api._handlers.tool_call).toBeDefined();
|
||||
expect(api._handlers.tool_call.length).toBe(1);
|
||||
});
|
||||
|
||||
it("blocks bash with curl and surfaces a reason", async () => {
|
||||
await registerOmpPlugin(api);
|
||||
const result = (await api._trigger("tool_call", {
|
||||
toolName: "bash",
|
||||
input: { command: "curl https://example.com/api" },
|
||||
})) as { block?: boolean; reason?: string } | undefined;
|
||||
|
||||
expect(result?.block).toBe(true);
|
||||
expect(result?.reason).toMatch(/context-mode/);
|
||||
});
|
||||
|
||||
it("blocks bash with wget", async () => {
|
||||
await registerOmpPlugin(api);
|
||||
const result = (await api._trigger("tool_call", {
|
||||
toolName: "bash",
|
||||
input: { command: "wget https://example.com/file" },
|
||||
})) as { block?: boolean } | undefined;
|
||||
expect(result?.block).toBe(true);
|
||||
});
|
||||
|
||||
it("blocks bash with inline node fetch", async () => {
|
||||
await registerOmpPlugin(api);
|
||||
const result = (await api._trigger("tool_call", {
|
||||
toolName: "bash",
|
||||
input: { command: "node -e \"fetch('https://api')\"" },
|
||||
})) as { block?: boolean } | undefined;
|
||||
expect(result?.block).toBe(true);
|
||||
});
|
||||
|
||||
it("blocks bash with python requests.get", async () => {
|
||||
await registerOmpPlugin(api);
|
||||
const result = (await api._trigger("tool_call", {
|
||||
toolName: "bash",
|
||||
input: { command: "python -c \"requests.get('https://api')\"" },
|
||||
})) as { block?: boolean } | undefined;
|
||||
expect(result?.block).toBe(true);
|
||||
});
|
||||
|
||||
it("blocks PowerShell Invoke-WebRequest", async () => {
|
||||
await registerOmpPlugin(api);
|
||||
const result = (await api._trigger("tool_call", {
|
||||
toolName: "bash",
|
||||
input: { command: "Invoke-WebRequest https://api" },
|
||||
})) as { block?: boolean } | undefined;
|
||||
expect(result?.block).toBe(true);
|
||||
});
|
||||
|
||||
it("does NOT block safe bash (git status)", async () => {
|
||||
await registerOmpPlugin(api);
|
||||
const result = await api._trigger("tool_call", {
|
||||
toolName: "bash",
|
||||
input: { command: "git status" },
|
||||
});
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it("does NOT block non-bash tools", async () => {
|
||||
await registerOmpPlugin(api);
|
||||
const result = await api._trigger("tool_call", {
|
||||
toolName: "edit",
|
||||
input: { file_path: "x.ts" },
|
||||
});
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it("tolerates malformed event payloads (no throw)", async () => {
|
||||
await registerOmpPlugin(api);
|
||||
// Missing input, missing toolName — must not throw, must passthrough
|
||||
await expect(api._trigger("tool_call", {})).resolves.toBeUndefined();
|
||||
await expect(api._trigger("tool_call", { toolName: "bash" })).resolves.toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// Slice 2: tool_result event extraction
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
describe("Slice 2: tool_result extraction", () => {
|
||||
it("registers a tool_result handler", async () => {
|
||||
await registerOmpPlugin(api);
|
||||
expect(api._handlers.tool_result).toBeDefined();
|
||||
});
|
||||
|
||||
it("persists a Read event into the session DB", async () => {
|
||||
await registerOmpPlugin(api);
|
||||
// Establish a session first so _sessionId is set
|
||||
await api._trigger("session_start", { type: "session_start" }, {});
|
||||
|
||||
await api._trigger("tool_result", {
|
||||
toolName: "read",
|
||||
input: { file_path: "/tmp/x.ts" },
|
||||
content: [{ type: "text", text: "export const x = 1;" }],
|
||||
});
|
||||
|
||||
// Verify event landed in DB at the OMP storage path
|
||||
const { OMPAdapter } = await import("../../src/adapters/omp/index.js");
|
||||
const adapter = new OMPAdapter();
|
||||
const db = new SessionDB({
|
||||
dbPath: join(adapter.getSessionDir(), "context-mode.db"),
|
||||
});
|
||||
const latest = db.getLatestSessionId();
|
||||
expect(latest).not.toBeNull();
|
||||
const events = db.getEvents(latest as string);
|
||||
expect(events.length).toBeGreaterThan(0);
|
||||
// file_read category should appear for a Read tool
|
||||
expect(events.some((e) => e.category === "file")).toBe(true);
|
||||
});
|
||||
|
||||
it("does nothing when no session has started", async () => {
|
||||
await registerOmpPlugin(api);
|
||||
// Trigger tool_result WITHOUT session_start first
|
||||
await expect(
|
||||
api._trigger("tool_result", {
|
||||
toolName: "read",
|
||||
input: { file_path: "/tmp/x.ts" },
|
||||
content: [{ type: "text", text: "x" }],
|
||||
}),
|
||||
).resolves.toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// Slice 3: session_start lifecycle
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
describe("Slice 3: session_start", () => {
|
||||
it("registers a session_start handler", async () => {
|
||||
await registerOmpPlugin(api);
|
||||
expect(api._handlers.session_start).toBeDefined();
|
||||
});
|
||||
|
||||
it("creates a session row in the DB", async () => {
|
||||
await registerOmpPlugin(api);
|
||||
await api._trigger("session_start", { type: "session_start" }, {});
|
||||
|
||||
const { OMPAdapter } = await import("../../src/adapters/omp/index.js");
|
||||
const adapter = new OMPAdapter();
|
||||
const db = new SessionDB({
|
||||
dbPath: join(adapter.getSessionDir(), "context-mode.db"),
|
||||
});
|
||||
const latest = db.getLatestSessionId();
|
||||
expect(latest).not.toBeNull();
|
||||
});
|
||||
|
||||
it("derives a stable session ID from sessionManager.getSessionFile when present", async () => {
|
||||
await registerOmpPlugin(api);
|
||||
await api._trigger("session_start", { type: "session_start" }, {
|
||||
sessionManager: { getSessionFile: () => "/path/to/session-abc.json" },
|
||||
});
|
||||
|
||||
const mod = await import("../../src/adapters/omp/plugin.js");
|
||||
const sid = mod._getOmpPluginSessionIdForTests();
|
||||
// 16-hex SHA-256 prefix per deriveSessionId contract
|
||||
expect(sid).toMatch(/^[a-f0-9]{16}$/);
|
||||
});
|
||||
});
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// Slice 4: session_before_compact resume snapshot
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
describe("Slice 4: session_before_compact", () => {
|
||||
it("registers a session_before_compact handler", async () => {
|
||||
await registerOmpPlugin(api);
|
||||
expect(api._handlers.session_before_compact).toBeDefined();
|
||||
});
|
||||
|
||||
it("persists a resume snapshot and increments compact_count", async () => {
|
||||
const mod = await registerOmpPlugin(api);
|
||||
await api._trigger("session_start", { type: "session_start" }, {});
|
||||
// Generate at least one event so the snapshot is non-empty
|
||||
await api._trigger("tool_result", {
|
||||
toolName: "read",
|
||||
input: { file_path: "/tmp/x.ts" },
|
||||
content: [{ type: "text", text: "x" }],
|
||||
});
|
||||
|
||||
await api._trigger("session_before_compact", { type: "session_before_compact" }, {});
|
||||
|
||||
// Read the session ID picked up by THIS test rather than the
|
||||
// shared-DB latest, which can collide at second-precision with
|
||||
// sibling test sessions.
|
||||
const pluginMod = await import("../../src/adapters/omp/plugin.js");
|
||||
const sid = pluginMod._getOmpPluginSessionIdForTests();
|
||||
expect(sid).not.toBe("");
|
||||
|
||||
const { OMPAdapter } = await import("../../src/adapters/omp/index.js");
|
||||
const adapter = new OMPAdapter();
|
||||
const db = new SessionDB({
|
||||
dbPath: join(adapter.getSessionDir(), "context-mode.db"),
|
||||
});
|
||||
const resume = db.getResume(sid);
|
||||
expect(resume).not.toBeNull();
|
||||
expect(resume?.snapshot.length).toBeGreaterThan(0);
|
||||
|
||||
const stats = db.getSessionStats(sid);
|
||||
expect(stats?.compact_count).toBe(1);
|
||||
void mod;
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user