oco-claude-plugin 0.1.0 → 0.2.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/README.md CHANGED
@@ -1,65 +1,153 @@
1
- # Open Context Orchestrator (OCO)
2
-
3
- Intelligent orchestration middleware for IDE-based coding assistants.
4
-
5
- OCO sits between your IDE, an LLM, local tools, and context sources. It decides at each step whether to respond, retrieve context, call a tool, verify a result, or stop — producing structured decision traces for full auditability.
6
-
7
- ## Architecture
8
-
9
- ```
10
- ┌──────────────┐ ┌──────────────────┐ ┌─────────────┐
11
- │ VS Code Ext │◄───►│ Orchestrator │◄───►│ ML Worker │
12
- │ (TypeScript)│ │ Core (Rust) │ │ (Python) │
13
- └──────────────┘ │ ┌────────────┐ │ └─────────────┘
14
- │ │ Policy Eng │ │
15
- │ │ Context Eng│ │ ┌─────────────┐
16
- │ │ Code Intel │ │◄───►│ LLM APIs │
17
- │ │ Tool RT │ │ │ (any) │
18
- │ │ Retrieval │ │ └─────────────┘
19
- │ │ Verifier │ │
20
- │ └────────────┘ │ ┌─────────────┐
21
- │ MCP Server │◄───►│ SQLite │
22
- └──────────────────┘ └─────────────┘
23
- ```
24
-
25
- ## Key Principles
26
-
27
- - **Provider-agnostic** — works with any LLM API
28
- - **Local-first** no cloud dependencies required
29
- - **Auditable** — every decision produces a structured trace
30
- - **Bounded** — explicit token, time, and tool-call budgets
31
- - **Graceful degradation** — works without ML components via heuristic fallbacks
32
-
33
- ## Stack
34
-
35
- | Layer | Technology |
36
- |-------|-----------|
37
- | Core runtime | Rust, Tokio, Axum |
38
- | Storage | SQLite + FTS5 |
39
- | Code analysis | Tree-sitter |
40
- | IPC | gRPC / Protobuf |
41
- | IDE extension | TypeScript, VS Code API |
42
- | ML worker | Python, Sentence Transformers |
43
- | Telemetry | tracing + OpenTelemetry |
44
-
45
- ## Getting Started
46
-
47
- ```bash
48
- # Prerequisites: Rust 1.85+, Node 20+, Python 3.11+, pnpm, uv
49
-
50
- # Build Rust crates
51
- cargo build
52
-
53
- # Setup Python ML worker
54
- cd py/ml-worker && uv sync
55
-
56
- # Setup VS Code extension
57
- cd apps/vscode-extension && pnpm install
58
-
59
- # Run dev CLI
60
- cargo run -p oco-dev-cli -- --help
61
- ```
62
-
63
- ## License
64
-
65
- Apache-2.0
1
+ # Open Context Orchestrator (OCO)
2
+
3
+ [![CI](https://github.com/hoklims/oco/actions/workflows/ci.yml/badge.svg)](https://github.com/hoklims/oco/actions/workflows/ci.yml)
4
+ [![npm](https://img.shields.io/npm/v/oco-claude-plugin)](https://www.npmjs.com/package/oco-claude-plugin)
5
+ [![License](https://img.shields.io/badge/license-Apache--2.0-blue)](LICENSE)
6
+
7
+ Intelligent orchestration middleware for IDE-based coding assistants.
8
+
9
+ OCO sits between your IDE, an LLM, local tools, and context sources. It decides at each step whether to respond, retrieve context, call a tool, verify a result, or stop — producing structured decision traces for full auditability.
10
+
11
+ ## Claude Code Plugin
12
+
13
+ Install OCO as a Claude Code plugin in any project — one command:
14
+
15
+ ```bash
16
+ npx oco-claude-plugin install # project-level
17
+ npx oco-claude-plugin install --global # all projects
18
+ npx oco-claude-plugin status # check installation
19
+ npx oco-claude-plugin uninstall # clean removal
20
+ ```
21
+
22
+ **What you get:**
23
+ - **Safety hooks** — blocks destructive commands, protects sensitive files, detects loops, enforces verification before completion
24
+ - **5 skills** — `/oco-inspect-repo-area`, `/oco-investigate-bug`, `/oco-safe-refactor`, `/oco-trace-stack`, `/oco-verify-fix`
25
+ - **3 agents** — `codebase-investigator`, `patch-verifier`, `refactor-reviewer`
26
+ - **MCP tools** — composite codebase search, error tracing, patch verification, findings collection
27
+
28
+ Works immediately with Node.js only. No API key required. Enhanced features available when the `oco` binary is installed.
29
+
30
+ ## Architecture
31
+
32
+ ```
33
+ ┌──────────────┐ ┌──────────────────┐ ┌─────────────┐
34
+ │ VS Code Ext │◄───►│ Orchestrator │◄───►│ ML Worker │
35
+ │ (TypeScript)│ │ Core (Rust) │ │ (Python) │
36
+ └──────────────┘ │ ┌────────────┐ │ └─────────────┘
37
+ │ │ Policy Eng │ │
38
+ │ │ Context Eng│ │ ┌─────────────┐
39
+ │ │ Code Intel │ │◄───►│ LLM APIs │
40
+ │ │ Tool RT │ │ │ (any) │
41
+ │ │ Retrieval │ │ └─────────────┘
42
+ │ │ Verifier │ │
43
+ │ └────────────┘ │ ┌─────────────┐
44
+ │ MCP Server │◄───►│ SQLite │
45
+ └──────────────────┘ └─────────────┘
46
+ ```
47
+
48
+ ## Key Principles
49
+
50
+ - **Provider-agnostic** works with Anthropic, Ollama, or any LLM API
51
+ - **Local-first** — no cloud dependencies required
52
+ - **Auditable** — every decision produces a structured trace
53
+ - **Bounded** explicit token, time, and tool-call budgets
54
+ - **Graceful degradation** — works without ML components via heuristic fallbacks
55
+ - **Deterministic policy** — no LLM calls for routing decisions
56
+ - **Event-driven UI** core emits structured events, CLI renders via pluggable renderers (Terminal/JSONL/Quiet)
57
+
58
+ ## Stack
59
+
60
+ | Layer | Technology |
61
+ |-------|-----------|
62
+ | Core runtime | Rust 1.85+, Tokio, Axum |
63
+ | Storage | SQLite + FTS5 |
64
+ | Code analysis | Tree-sitter (regex fallback) |
65
+ | IPC | gRPC / Protobuf |
66
+ | IDE extension | TypeScript, VS Code API |
67
+ | ML worker | Python, Sentence Transformers |
68
+ | Telemetry | tracing + OpenTelemetry |
69
+
70
+ ## Getting Started
71
+
72
+ ### Prerequisites
73
+
74
+ - Rust 1.85+ (edition 2024)
75
+ - Node 20+ (for Claude Code plugin)
76
+ - Python 3.11+ and uv (optional, for ML worker)
77
+ - pnpm (optional, for VS Code extension)
78
+
79
+ ### Build from source
80
+
81
+ ```bash
82
+ git clone https://github.com/hoklims/oco.git
83
+ cd oco
84
+
85
+ # Build all Rust crates
86
+ cargo build
87
+
88
+ # Run the test suite (226+ tests)
89
+ cargo test
90
+
91
+ # Run the CLI
92
+ cargo run -p oco-dev-cli -- --help
93
+ ```
94
+
95
+ ### Optional components
96
+
97
+ ```bash
98
+ # Python ML worker (embeddings & reranking)
99
+ cd py/ml-worker && uv sync
100
+
101
+ # VS Code extension
102
+ cd apps/vscode-extension && pnpm install
103
+ ```
104
+
105
+ ### Quick usage
106
+
107
+ ```bash
108
+ oco index ./my-project # Index a workspace
109
+ oco search "auth handler" --workspace . # Full-text search
110
+ oco run "fix the login bug" --workspace . # Orchestrate (live trace)
111
+ oco serve --port 3000 # Start HTTP/MCP server
112
+ oco doctor --workspace . # Check health
113
+ oco eval scenarios.jsonl # Run evaluations
114
+ oco runs list # List past runs
115
+ oco runs show last # Replay last run's trace
116
+ ```
117
+
118
+ ### Output modes
119
+
120
+ ```bash
121
+ oco doctor # Human: colors, spinners, icons
122
+ oco --format jsonl doctor # Machine: 1 JSON event per line
123
+ oco --quiet doctor # Quiet: only final result/errors
124
+ ```
125
+
126
+ ## Configuration
127
+
128
+ Runtime config in `oco.toml`. See [`examples/oco.toml`](examples/oco.toml) for a documented template.
129
+
130
+ ### LLM Providers
131
+
132
+ | Provider | Config | Requirements |
133
+ |----------|--------|--------------|
134
+ | `stub` | `provider = "stub"` | None |
135
+ | `anthropic` | `provider = "anthropic"` | `ANTHROPIC_API_KEY` env var |
136
+ | `ollama` | `provider = "ollama"` | Local Ollama at `localhost:11434` |
137
+
138
+ ## Documentation
139
+
140
+ - [Architecture overview](docs/architecture/overview.md)
141
+ - [Architecture Decision Records](docs/adr/)
142
+ - [Feature specifications](docs/specs/)
143
+ - [Claude Code integration](.claude/README.md)
144
+ - [Contributing](CONTRIBUTING.md)
145
+ - [Security policy](SECURITY.md)
146
+
147
+ ## Contributing
148
+
149
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for setup instructions, conventions, and workflow.
150
+
151
+ ## License
152
+
153
+ [Apache-2.0](LICENSE)
package/cli.mjs CHANGED
@@ -3,9 +3,9 @@
3
3
  * OCO Claude Code Plugin — Installer / Uninstaller
4
4
  *
5
5
  * Usage:
6
- * npx github:hoklims/oco install [--global] [--force]
7
- * npx github:hoklims/oco uninstall [--global]
8
- * npx github:hoklims/oco status [--global]
6
+ * npx oco-claude-plugin install [--global] [--force]
7
+ * npx oco-claude-plugin uninstall [--global]
8
+ * npx oco-claude-plugin status [--global]
9
9
  *
10
10
  * Zero external dependencies. Node >= 18.
11
11
  */
@@ -232,9 +232,9 @@ function usage() {
232
232
  --force, -f Overwrite existing files
233
233
 
234
234
  Examples:
235
- npx github:hoklims/oco install # project-level
236
- npx github:hoklims/oco install -g # global
237
- npx github:hoklims/oco uninstall # clean removal
235
+ npx oco-claude-plugin install # project-level
236
+ npx oco-claude-plugin install -g # global
237
+ npx oco-claude-plugin uninstall # clean removal
238
238
  `);
239
239
  }
240
240
 
package/package.json CHANGED
@@ -1,28 +1,28 @@
1
- {
2
- "name": "oco-claude-plugin",
3
- "version": "0.1.0",
4
- "description": "OCO Claude Code plugin — safety hooks, skills, agents, and MCP tools for any project",
5
- "type": "module",
6
- "bin": {
7
- "oco-plugin": "./cli.mjs"
8
- },
9
- "files": [
10
- "cli.mjs",
11
- "plugin/"
12
- ],
13
- "engines": {
14
- "node": ">=18"
15
- },
16
- "repository": {
17
- "type": "git",
18
- "url": "https://github.com/hoklims/oco.git"
19
- },
20
- "license": "Apache-2.0",
21
- "keywords": [
22
- "claude-code",
23
- "mcp",
24
- "orchestrator",
25
- "code-intelligence",
26
- "developer-tools"
27
- ]
28
- }
1
+ {
2
+ "name": "oco-claude-plugin",
3
+ "version": "0.2.0",
4
+ "description": "OCO Claude Code plugin — safety hooks, skills, agents, and MCP tools for any project",
5
+ "type": "module",
6
+ "bin": {
7
+ "oco-plugin": "./cli.mjs"
8
+ },
9
+ "files": [
10
+ "cli.mjs",
11
+ "plugin/"
12
+ ],
13
+ "engines": {
14
+ "node": ">=18"
15
+ },
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "https://github.com/hoklims/oco.git"
19
+ },
20
+ "license": "Apache-2.0",
21
+ "keywords": [
22
+ "claude-code",
23
+ "mcp",
24
+ "orchestrator",
25
+ "code-intelligence",
26
+ "developer-tools"
27
+ ]
28
+ }
@@ -1,434 +1,468 @@
1
- #!/usr/bin/env node
2
- /**
3
- * OCO MCP Bridge Server
4
- *
5
- * Minimal MCP server that bridges Claude Code to the local OCO runtime.
6
- * Exposes only composite, high-value tools.
7
- *
8
- * Transport: stdio (Claude Code spawns this process)
9
- * Backend: calls local `oco` CLI binary
10
- */
11
-
12
- const { spawn } = require("child_process");
13
- const readline = require("readline");
14
-
15
- const OCO_BIN = process.env.OCO_BIN || "oco";
16
- const WORKSPACE = process.env.OCO_WORKSPACE || process.cwd();
17
-
18
- // --- MCP Protocol Handler ---
19
-
20
- const rl = readline.createInterface({ input: process.stdin });
21
- let buffer = "";
22
-
23
- rl.on("line", (line) => {
24
- try {
25
- const request = JSON.parse(line);
26
- handleRequest(request).then((response) => {
27
- process.stdout.write(JSON.stringify(response) + "\n");
28
- });
29
- } catch {
30
- // Ignore malformed lines
31
- }
32
- });
33
-
34
- async function handleRequest(request) {
35
- const { id, method, params } = request;
36
-
37
- switch (method) {
38
- case "initialize":
39
- return success(id, {
40
- protocolVersion: "2024-11-05",
41
- serverInfo: { name: "oco-bridge", version: "0.1.0" },
42
- capabilities: {
43
- tools: { listChanged: false },
44
- },
45
- });
46
-
47
- case "tools/list":
48
- return success(id, { tools: TOOLS });
49
-
50
- case "tools/call":
51
- return handleToolCall(id, params.name, params.arguments || {});
52
-
53
- default:
54
- return error(id, -32601, `Method not found: ${method}`);
55
- }
56
- }
57
-
58
- // --- Tool Definitions ---
59
-
60
- const TOOLS = [
61
- {
62
- name: "oco.search_codebase",
63
- description:
64
- "Composite codebase search: lexical + structural ranking with symbol-aware narrowing. Returns compact ranked results.",
65
- inputSchema: {
66
- type: "object",
67
- properties: {
68
- query: {
69
- type: "string",
70
- description: "Search query (natural language or symbol name)",
71
- },
72
- workspace: {
73
- type: "string",
74
- description: "Workspace root path (defaults to cwd)",
75
- },
76
- limit: {
77
- type: "integer",
78
- description: "Max results (default: 10)",
79
- default: 10,
80
- },
81
- },
82
- required: ["query"],
83
- },
84
- },
85
- {
86
- name: "oco.trace_error",
87
- description:
88
- "Composite error analysis: maps stack trace to codebase, identifies likely root cause regions, suggests next verification step.",
89
- inputSchema: {
90
- type: "object",
91
- properties: {
92
- stacktrace: {
93
- type: "string",
94
- description: "The stack trace or error output to analyze",
95
- },
96
- workspace: {
97
- type: "string",
98
- description: "Workspace root path",
99
- },
100
- },
101
- required: ["stacktrace"],
102
- },
103
- },
104
- {
105
- name: "oco.verify_patch",
106
- description:
107
- "Composite verification: detects project type, runs build/test/lint/typecheck, returns structured verdict.",
108
- inputSchema: {
109
- type: "object",
110
- properties: {
111
- workspace: {
112
- type: "string",
113
- description: "Workspace root path",
114
- },
115
- checks: {
116
- type: "array",
117
- items: { type: "string" },
118
- description:
119
- "Specific checks to run (build, test, lint, typecheck). Defaults to all available.",
120
- },
121
- },
122
- },
123
- },
124
- {
125
- name: "oco.collect_findings",
126
- description:
127
- "Composite state extraction: current evidence, open questions, unresolved risks, suggested next action from the OCO session.",
128
- inputSchema: {
129
- type: "object",
130
- properties: {
131
- session_id: {
132
- type: "string",
133
- description: "OCO session ID (optional, uses latest if omitted)",
134
- },
135
- },
136
- },
137
- },
138
- ];
139
-
140
- // --- Tool Handlers ---
141
-
142
- async function handleToolCall(id, toolName, args) {
143
- try {
144
- switch (toolName) {
145
- case "oco.search_codebase":
146
- return await searchCodebase(id, args);
147
- case "oco.trace_error":
148
- return await traceError(id, args);
149
- case "oco.verify_patch":
150
- return await verifyPatch(id, args);
151
- case "oco.collect_findings":
152
- return await collectFindings(id, args);
153
- default:
154
- return error(id, -32601, `Unknown tool: ${toolName}`);
155
- }
156
- } catch (e) {
157
- return success(id, {
158
- content: [{ type: "text", text: `Error: ${e.message}` }],
159
- isError: true,
160
- });
161
- }
162
- }
163
-
164
- async function searchCodebase(id, args) {
165
- const workspace = args.workspace || WORKSPACE;
166
- const limit = args.limit || 10;
167
-
168
- const result = await runOco([
169
- "search",
170
- args.query,
171
- "--workspace",
172
- workspace,
173
- "--limit",
174
- String(limit),
175
- "--format",
176
- "json",
177
- ]);
178
-
179
- if (result.error) {
180
- // Graceful degradation: return empty results
181
- return success(id, {
182
- content: [
183
- {
184
- type: "text",
185
- text: JSON.stringify({ results: [], note: "OCO backend unavailable, use standard search tools" }),
186
- },
187
- ],
188
- });
189
- }
190
-
191
- return success(id, {
192
- content: [{ type: "text", text: result.stdout }],
193
- });
194
- }
195
-
196
- async function traceError(id, args) {
197
- const workspace = args.workspace || WORKSPACE;
198
-
199
- // Parse stack trace to extract file paths and line numbers
200
- const frames = parseStackTrace(args.stacktrace);
201
-
202
- if (frames.length === 0) {
203
- return success(id, {
204
- content: [
205
- {
206
- type: "text",
207
- text: JSON.stringify({
208
- frames: [],
209
- note: "Could not parse stack trace. Provide the raw error output.",
210
- }),
211
- },
212
- ],
213
- });
214
- }
215
-
216
- // Search for each unique file in the stack trace
217
- const fileSet = [...new Set(frames.map((f) => f.file))];
218
- const results = [];
219
-
220
- for (const file of fileSet.slice(0, 5)) {
221
- const search = await runOco([
222
- "search",
223
- file,
224
- "--workspace",
225
- workspace,
226
- "--limit",
227
- "3",
228
- "--format",
229
- "json",
230
- ]);
231
- if (!search.error && search.stdout) {
232
- try {
233
- const parsed = JSON.parse(search.stdout);
234
- results.push({ file, matches: parsed });
235
- } catch {
236
- // skip
237
- }
238
- }
239
- }
240
-
241
- return success(id, {
242
- content: [
243
- {
244
- type: "text",
245
- text: JSON.stringify({
246
- parsed_frames: frames,
247
- codebase_matches: results,
248
- suggestion: "Inspect the deepest application frame first. Check for null access, type errors, or missing validation.",
249
- }),
250
- },
251
- ],
252
- });
253
- }
254
-
255
- async function verifyPatch(id, args) {
256
- const workspace = args.workspace || WORKSPACE;
257
- const checks = args.checks || ["build", "test", "lint", "typecheck"];
258
-
259
- const verdicts = {};
260
-
261
- for (const check of checks) {
262
- const cmd = getCheckCommand(workspace, check);
263
- if (!cmd) {
264
- verdicts[check] = { status: "skip", reason: "not available" };
265
- continue;
266
- }
267
-
268
- const result = await runShell(cmd.command, cmd.args, { cwd: workspace });
269
- const passed = result.exitCode === 0;
270
- verdicts[check] = {
271
- status: passed ? "pass" : "fail",
272
- // Only include output on failure to avoid leaking noisy stderr warnings
273
- ...(passed ? {} : { output: truncate((result.stderr + "\n" + result.stdout).trim(), 500) }),
274
- };
275
-
276
- // Stop on first failure
277
- if (result.exitCode !== 0) {
278
- break;
279
- }
280
- }
281
-
282
- const entries = Object.values(verdicts);
283
- const allSkipped = entries.every((v) => v.status === "skip");
284
- const hasFail = entries.some((v) => v.status === "fail");
285
- const verdict = hasFail ? "FAIL" : allSkipped ? "SKIP" : "PASS";
286
-
287
- return success(id, {
288
- content: [
289
- {
290
- type: "text",
291
- text: JSON.stringify({
292
- verdict,
293
- checks: verdicts,
294
- ...(allSkipped && { note: "No verification commands available for this workspace. Manual review required." }),
295
- }),
296
- },
297
- ],
298
- });
299
- }
300
-
301
- async function collectFindings(id, args) {
302
- const sessionId = args.session_id || "latest";
303
-
304
- const result = await runOco([
305
- "trace",
306
- sessionId,
307
- "--format",
308
- "json",
309
- ]);
310
-
311
- if (result.error) {
312
- return success(id, {
313
- content: [
314
- {
315
- type: "text",
316
- text: JSON.stringify({
317
- evidence: [],
318
- open_questions: [],
319
- risks: [],
320
- next_action: "No OCO session data available. Use standard investigation.",
321
- }),
322
- },
323
- ],
324
- });
325
- }
326
-
327
- return success(id, {
328
- content: [{ type: "text", text: result.stdout }],
329
- });
330
- }
331
-
332
- // --- Helpers ---
333
-
334
- function parseStackTrace(text) {
335
- const frames = [];
336
- // Common patterns: file:line, file(line), at file:line:col
337
- const patterns = [
338
- /at\s+(?:\w+\s+\()?([^:(\s]+):(\d+)/g, // JS/TS: at func (file:line:col)
339
- /File "([^"]+)", line (\d+)/g, // Python: File "path", line N
340
- /([^\s]+\.rs):(\d+)/g, // Rust: file.rs:line
341
- /([^\s]+\.[a-z]+):(\d+)/g, // Generic: file.ext:line
342
- ];
343
-
344
- for (const pattern of patterns) {
345
- let match;
346
- while ((match = pattern.exec(text)) !== null) {
347
- frames.push({ file: match[1], line: parseInt(match[2], 10) });
348
- }
349
- }
350
-
351
- return frames;
352
- }
353
-
354
- function getCheckCommand(workspace, check) {
355
- const fs = require("fs");
356
- const path = require("path");
357
-
358
- const hasFile = (name) =>
359
- fs.existsSync(path.join(workspace, name));
360
-
361
- switch (check) {
362
- case "build":
363
- if (hasFile("Cargo.toml"))
364
- return { command: "cargo", args: ["build"] };
365
- if (hasFile("package.json"))
366
- return { command: "npm", args: ["run", "build"] };
367
- return null;
368
- case "test":
369
- if (hasFile("Cargo.toml"))
370
- return { command: "cargo", args: ["test"] };
371
- if (hasFile("package.json"))
372
- return { command: "npm", args: ["test"] };
373
- if (hasFile("pyproject.toml"))
374
- return { command: "pytest", args: [] };
375
- return null;
376
- case "lint":
377
- if (hasFile("Cargo.toml"))
378
- return { command: "cargo", args: ["clippy", "--", "-D", "warnings"] };
379
- if (hasFile("package.json"))
380
- return { command: "npm", args: ["run", "lint"] };
381
- return null;
382
- case "typecheck":
383
- if (hasFile("Cargo.toml"))
384
- return { command: "cargo", args: ["check"] };
385
- if (hasFile("tsconfig.json"))
386
- return { command: "npx", args: ["tsc", "--noEmit"] };
387
- if (hasFile("pyproject.toml"))
388
- return { command: "mypy", args: ["."] };
389
- return null;
390
- default:
391
- return null;
392
- }
393
- }
394
-
395
- function runOco(args) {
396
- return runShell(OCO_BIN, args, {});
397
- }
398
-
399
- function runShell(command, args, options) {
400
- return new Promise((resolve) => {
401
- const proc = spawn(command, args, {
402
- ...options,
403
- timeout: 30000,
404
- stdio: ["ignore", "pipe", "pipe"],
405
- });
406
-
407
- let stdout = "";
408
- let stderr = "";
409
-
410
- proc.stdout.on("data", (d) => (stdout += d.toString()));
411
- proc.stderr.on("data", (d) => (stderr += d.toString()));
412
-
413
- proc.on("close", (code) => {
414
- resolve({ exitCode: code, stdout, stderr, error: null });
415
- });
416
-
417
- proc.on("error", (err) => {
418
- resolve({ exitCode: -1, stdout: "", stderr: "", error: err.message });
419
- });
420
- });
421
- }
422
-
423
- function truncate(str, maxLen) {
424
- if (!str || str.length <= maxLen) return str || "";
425
- return str.slice(0, maxLen) + "\n... (truncated)";
426
- }
427
-
428
- function success(id, result) {
429
- return { jsonrpc: "2.0", id, result };
430
- }
431
-
432
- function error(id, code, message) {
433
- return { jsonrpc: "2.0", id, error: { code, message } };
434
- }
1
+ #!/usr/bin/env node
2
+ /**
3
+ * OCO MCP Bridge Server
4
+ *
5
+ * Minimal MCP server that bridges Claude Code to the local OCO runtime.
6
+ * Exposes only composite, high-value tools.
7
+ *
8
+ * Transport: stdio (Claude Code spawns this process)
9
+ * Backend: calls local `oco` CLI binary
10
+ */
11
+
12
+ const { spawn } = require("child_process");
13
+ const readline = require("readline");
14
+
15
+ const OCO_BIN = process.env.OCO_BIN || "oco";
16
+ const WORKSPACE = process.env.OCO_WORKSPACE || process.cwd();
17
+
18
+ // --- MCP Protocol Handler ---
19
+
20
+ const rl = readline.createInterface({ input: process.stdin });
21
+ let buffer = "";
22
+
23
+ rl.on("line", (line) => {
24
+ try {
25
+ const request = JSON.parse(line);
26
+ handleRequest(request).then((response) => {
27
+ process.stdout.write(JSON.stringify(response) + "\n");
28
+ });
29
+ } catch {
30
+ // Ignore malformed lines
31
+ }
32
+ });
33
+
34
+ async function handleRequest(request) {
35
+ const { id, method, params } = request;
36
+
37
+ switch (method) {
38
+ case "initialize":
39
+ return success(id, {
40
+ protocolVersion: "2024-11-05",
41
+ serverInfo: { name: "oco-bridge", version: "0.1.0" },
42
+ capabilities: {
43
+ tools: { listChanged: false },
44
+ },
45
+ });
46
+
47
+ case "tools/list":
48
+ return success(id, { tools: TOOLS });
49
+
50
+ case "tools/call":
51
+ return handleToolCall(id, params.name, params.arguments || {});
52
+
53
+ default:
54
+ return error(id, -32601, `Method not found: ${method}`);
55
+ }
56
+ }
57
+
58
+ // --- Tool Definitions ---
59
+
60
+ const TOOLS = [
61
+ {
62
+ name: "oco.search_codebase",
63
+ description:
64
+ "Composite codebase search: lexical + structural ranking with symbol-aware narrowing. Returns compact ranked results.",
65
+ inputSchema: {
66
+ type: "object",
67
+ properties: {
68
+ query: {
69
+ type: "string",
70
+ description: "Search query (natural language or symbol name)",
71
+ },
72
+ workspace: {
73
+ type: "string",
74
+ description: "Workspace root path (defaults to cwd)",
75
+ },
76
+ limit: {
77
+ type: "integer",
78
+ description: "Max results (default: 10)",
79
+ default: 10,
80
+ },
81
+ },
82
+ required: ["query"],
83
+ },
84
+ },
85
+ {
86
+ name: "oco.trace_error",
87
+ description:
88
+ "Composite error analysis: maps stack trace to codebase, identifies likely root cause regions, suggests next verification step.",
89
+ inputSchema: {
90
+ type: "object",
91
+ properties: {
92
+ stacktrace: {
93
+ type: "string",
94
+ description: "The stack trace or error output to analyze",
95
+ },
96
+ workspace: {
97
+ type: "string",
98
+ description: "Workspace root path",
99
+ },
100
+ },
101
+ required: ["stacktrace"],
102
+ },
103
+ },
104
+ {
105
+ name: "oco.verify_patch",
106
+ description:
107
+ "Composite verification: detects project type, runs build/test/lint/typecheck, returns structured verdict.",
108
+ inputSchema: {
109
+ type: "object",
110
+ properties: {
111
+ workspace: {
112
+ type: "string",
113
+ description: "Workspace root path",
114
+ },
115
+ checks: {
116
+ type: "array",
117
+ items: { type: "string" },
118
+ description:
119
+ "Specific checks to run (build, test, lint, typecheck). Defaults to all available.",
120
+ },
121
+ },
122
+ },
123
+ },
124
+ {
125
+ name: "oco.collect_findings",
126
+ description:
127
+ "Composite state extraction: current evidence, open questions, unresolved risks, suggested next action from the OCO session.",
128
+ inputSchema: {
129
+ type: "object",
130
+ properties: {
131
+ session_id: {
132
+ type: "string",
133
+ description: "OCO session ID (optional, uses latest if omitted)",
134
+ },
135
+ },
136
+ },
137
+ },
138
+ ];
139
+
140
+ // --- Tool Handlers ---
141
+
142
+ async function handleToolCall(id, toolName, args) {
143
+ try {
144
+ switch (toolName) {
145
+ case "oco.search_codebase":
146
+ return await searchCodebase(id, args);
147
+ case "oco.trace_error":
148
+ return await traceError(id, args);
149
+ case "oco.verify_patch":
150
+ return await verifyPatch(id, args);
151
+ case "oco.collect_findings":
152
+ return await collectFindings(id, args);
153
+ default:
154
+ return error(id, -32601, `Unknown tool: ${toolName}`);
155
+ }
156
+ } catch (e) {
157
+ return success(id, {
158
+ content: [{ type: "text", text: `Error: ${e.message}` }],
159
+ isError: true,
160
+ });
161
+ }
162
+ }
163
+
164
+ async function searchCodebase(id, args) {
165
+ const workspace = args.workspace || WORKSPACE;
166
+ const limit = args.limit || 10;
167
+
168
+ const result = await runOco([
169
+ "search",
170
+ args.query,
171
+ "--workspace",
172
+ workspace,
173
+ "--limit",
174
+ String(limit),
175
+ "--format",
176
+ "json",
177
+ ]);
178
+
179
+ if (result.error) {
180
+ return respondStructured(id, {
181
+ summary: "OCO backend unavailable",
182
+ evidence: [],
183
+ risks: ["Search results may be incomplete without OCO indexing"],
184
+ next_step: "Use standard search tools (Grep, Glob) as fallback",
185
+ confidence: 0.0,
186
+ });
187
+ }
188
+
189
+ let parsed = [];
190
+ try { parsed = JSON.parse(result.stdout); } catch { /* keep empty */ }
191
+ const results = Array.isArray(parsed) ? parsed : (parsed.results || []);
192
+
193
+ return respondStructured(id, {
194
+ summary: `Found ${results.length} result(s) for "${args.query}"`,
195
+ evidence: results.slice(0, limit),
196
+ risks: [],
197
+ next_step: results.length > 0
198
+ ? "Review top results and inspect relevant files"
199
+ : "Broaden search query or try different keywords",
200
+ confidence: results.length > 0 ? 0.8 : 0.2,
201
+ });
202
+ }
203
+
204
+ async function traceError(id, args) {
205
+ const workspace = args.workspace || WORKSPACE;
206
+
207
+ // Parse stack trace to extract file paths and line numbers
208
+ const frames = parseStackTrace(args.stacktrace);
209
+
210
+ if (frames.length === 0) {
211
+ return respondStructured(id, {
212
+ summary: "Could not parse stack trace",
213
+ evidence: [],
214
+ risks: ["Raw error output may need manual analysis"],
215
+ next_step: "Provide the full raw error output for better parsing",
216
+ confidence: 0.1,
217
+ });
218
+ }
219
+
220
+ // Search for each unique file in the stack trace
221
+ const fileSet = [...new Set(frames.map((f) => f.file))];
222
+ const results = [];
223
+
224
+ for (const file of fileSet.slice(0, 5)) {
225
+ const search = await runOco([
226
+ "search",
227
+ file,
228
+ "--workspace",
229
+ workspace,
230
+ "--limit",
231
+ "3",
232
+ "--format",
233
+ "json",
234
+ ]);
235
+ if (!search.error && search.stdout) {
236
+ try {
237
+ const parsed = JSON.parse(search.stdout);
238
+ results.push({ file, matches: parsed });
239
+ } catch {
240
+ // skip
241
+ }
242
+ }
243
+ }
244
+
245
+ const deepestFrame = frames[frames.length - 1];
246
+ const matchedFiles = results.filter((r) => r.matches && (Array.isArray(r.matches) ? r.matches.length > 0 : true));
247
+
248
+ return respondStructured(id, {
249
+ summary: `Parsed ${frames.length} frame(s) across ${fileSet.length} file(s). ${matchedFiles.length} matched in codebase.`,
250
+ evidence: [
251
+ { parsed_frames: frames },
252
+ { codebase_matches: results },
253
+ ],
254
+ risks: matchedFiles.length === 0
255
+ ? ["No stack frames matched local files — error may originate in dependencies"]
256
+ : [],
257
+ next_step: deepestFrame
258
+ ? `Inspect ${deepestFrame.file}:${deepestFrame.line} — deepest application frame`
259
+ : "Review the stack trace manually",
260
+ confidence: matchedFiles.length > 0 ? 0.7 : 0.3,
261
+ });
262
+ }
263
+
264
+ async function verifyPatch(id, args) {
265
+ const workspace = args.workspace || WORKSPACE;
266
+ const checks = args.checks || ["build", "test", "lint", "typecheck"];
267
+
268
+ const verdicts = {};
269
+
270
+ for (const check of checks) {
271
+ const cmd = getCheckCommand(workspace, check);
272
+ if (!cmd) {
273
+ verdicts[check] = { status: "skip", reason: "not available" };
274
+ continue;
275
+ }
276
+
277
+ const result = await runShell(cmd.command, cmd.args, { cwd: workspace });
278
+ const passed = result.exitCode === 0;
279
+ verdicts[check] = {
280
+ status: passed ? "pass" : "fail",
281
+ ...(passed ? {} : { output: truncate((result.stderr + "\n" + result.stdout).trim(), 500) }),
282
+ };
283
+
284
+ // Stop on first failure
285
+ if (result.exitCode !== 0) {
286
+ break;
287
+ }
288
+ }
289
+
290
+ const entries = Object.values(verdicts);
291
+ const allSkipped = entries.every((v) => v.status === "skip");
292
+ const hasFail = entries.some((v) => v.status === "fail");
293
+ const verdict = hasFail ? "FAIL" : allSkipped ? "SKIP" : "PASS";
294
+ const failedChecks = Object.entries(verdicts).filter(([, v]) => v.status === "fail").map(([k]) => k);
295
+ const passedChecks = Object.entries(verdicts).filter(([, v]) => v.status === "pass").map(([k]) => k);
296
+
297
+ return respondStructured(id, {
298
+ summary: `Verification ${verdict}: ${passedChecks.length} passed, ${failedChecks.length} failed, ${entries.length - passedChecks.length - failedChecks.length} skipped`,
299
+ evidence: [{ verdict, checks: verdicts }],
300
+ risks: hasFail
301
+ ? failedChecks.map((c) => `${c} failed — see output for details`)
302
+ : allSkipped
303
+ ? ["No verification commands detected for this workspace"]
304
+ : [],
305
+ next_step: hasFail
306
+ ? `Fix ${failedChecks[0]} errors first, then re-verify`
307
+ : allSkipped
308
+ ? "Configure build/test/lint commands or verify manually"
309
+ : "All checks passed — safe to proceed",
310
+ confidence: hasFail ? 0.9 : allSkipped ? 0.1 : 1.0,
311
+ });
312
+ }
313
+
314
+ async function collectFindings(id, args) {
315
+ const sessionId = args.session_id || "latest";
316
+
317
+ const result = await runOco([
318
+ "trace",
319
+ sessionId,
320
+ "--format",
321
+ "json",
322
+ ]);
323
+
324
+ if (result.error) {
325
+ return respondStructured(id, {
326
+ summary: "No OCO session data available",
327
+ evidence: [],
328
+ risks: ["Session trace unavailable investigation state unknown"],
329
+ next_step: "Use standard investigation tools to gather evidence",
330
+ confidence: 0.0,
331
+ });
332
+ }
333
+
334
+ let trace = [];
335
+ try { trace = JSON.parse(result.stdout); } catch { /* keep empty */ }
336
+ // Unwrap { traces: [...] } envelope if present.
337
+ const traceEntries = Array.isArray(trace)
338
+ ? trace
339
+ : Array.isArray(trace?.traces)
340
+ ? trace.traces
341
+ : [trace];
342
+ const errors = traceEntries.filter((t) => t.decision_type === "error" || t.error);
343
+ const decisions = traceEntries.filter((t) => t.reasoning);
344
+
345
+ return respondStructured(id, {
346
+ summary: `Session ${sessionId}: ${traceEntries.length} trace entries, ${errors.length} error(s), ${decisions.length} decision(s)`,
347
+ evidence: traceEntries,
348
+ risks: errors.map((e) => e.error || e.reasoning || "Unknown error in trace"),
349
+ next_step: errors.length > 0
350
+ ? "Investigate unresolved errors in the session trace"
351
+ : "Review decisions for correctness and proceed",
352
+ confidence: errors.length === 0 ? 0.8 : 0.5,
353
+ });
354
+ }
355
+
356
+ // --- Helpers ---
357
+
358
+ function parseStackTrace(text) {
359
+ const frames = [];
360
+ // Common patterns: file:line, file(line), at file:line:col
361
+ const patterns = [
362
+ /at\s+(?:\w+\s+\()?([^:(\s]+):(\d+)/g, // JS/TS: at func (file:line:col)
363
+ /File "([^"]+)", line (\d+)/g, // Python: File "path", line N
364
+ /([^\s]+\.rs):(\d+)/g, // Rust: file.rs:line
365
+ /([^\s]+\.[a-z]+):(\d+)/g, // Generic: file.ext:line
366
+ ];
367
+
368
+ for (const pattern of patterns) {
369
+ let match;
370
+ while ((match = pattern.exec(text)) !== null) {
371
+ frames.push({ file: match[1], line: parseInt(match[2], 10) });
372
+ }
373
+ }
374
+
375
+ return frames;
376
+ }
377
+
378
+ function getCheckCommand(workspace, check) {
379
+ const fs = require("fs");
380
+ const path = require("path");
381
+
382
+ const hasFile = (name) =>
383
+ fs.existsSync(path.join(workspace, name));
384
+
385
+ switch (check) {
386
+ case "build":
387
+ if (hasFile("Cargo.toml"))
388
+ return { command: "cargo", args: ["build"] };
389
+ if (hasFile("package.json"))
390
+ return { command: "npm", args: ["run", "build"] };
391
+ return null;
392
+ case "test":
393
+ if (hasFile("Cargo.toml"))
394
+ return { command: "cargo", args: ["test"] };
395
+ if (hasFile("package.json"))
396
+ return { command: "npm", args: ["test"] };
397
+ if (hasFile("pyproject.toml"))
398
+ return { command: "pytest", args: [] };
399
+ return null;
400
+ case "lint":
401
+ if (hasFile("Cargo.toml"))
402
+ return { command: "cargo", args: ["clippy", "--", "-D", "warnings"] };
403
+ if (hasFile("package.json"))
404
+ return { command: "npm", args: ["run", "lint"] };
405
+ return null;
406
+ case "typecheck":
407
+ if (hasFile("Cargo.toml"))
408
+ return { command: "cargo", args: ["check"] };
409
+ if (hasFile("tsconfig.json"))
410
+ return { command: "npx", args: ["tsc", "--noEmit"] };
411
+ if (hasFile("pyproject.toml"))
412
+ return { command: "mypy", args: ["."] };
413
+ return null;
414
+ default:
415
+ return null;
416
+ }
417
+ }
418
+
419
+ function runOco(args) {
420
+ return runShell(OCO_BIN, args, {});
421
+ }
422
+
423
+ function runShell(command, args, options) {
424
+ return new Promise((resolve) => {
425
+ const proc = spawn(command, args, {
426
+ ...options,
427
+ timeout: 30000,
428
+ stdio: ["ignore", "pipe", "pipe"],
429
+ });
430
+
431
+ let stdout = "";
432
+ let stderr = "";
433
+
434
+ proc.stdout.on("data", (d) => (stdout += d.toString()));
435
+ proc.stderr.on("data", (d) => (stderr += d.toString()));
436
+
437
+ proc.on("close", (code) => {
438
+ resolve({ exitCode: code, stdout, stderr, error: null });
439
+ });
440
+
441
+ proc.on("error", (err) => {
442
+ resolve({ exitCode: -1, stdout: "", stderr: "", error: err.message });
443
+ });
444
+ });
445
+ }
446
+
447
+ function truncate(str, maxLen) {
448
+ if (!str || str.length <= maxLen) return str || "";
449
+ return str.slice(0, maxLen) + "\n... (truncated)";
450
+ }
451
+
452
+ /**
453
+ * Wrap a structured response in MCP format.
454
+ * All tools return: { summary, evidence, risks, next_step, confidence }
455
+ */
456
+ function respondStructured(id, payload) {
457
+ return success(id, {
458
+ content: [{ type: "text", text: JSON.stringify(payload) }],
459
+ });
460
+ }
461
+
462
+ function success(id, result) {
463
+ return { jsonrpc: "2.0", id, result };
464
+ }
465
+
466
+ function error(id, code, message) {
467
+ return { jsonrpc: "2.0", id, error: { code, message } };
468
+ }