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 +153 -65
- package/cli.mjs +6 -6
- package/package.json +28 -28
- package/plugin/mcp/bridge.js +468 -434
package/README.md
CHANGED
|
@@ -1,65 +1,153 @@
|
|
|
1
|
-
# Open Context Orchestrator (OCO)
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
1
|
+
# Open Context Orchestrator (OCO)
|
|
2
|
+
|
|
3
|
+
[](https://github.com/hoklims/oco/actions/workflows/ci.yml)
|
|
4
|
+
[](https://www.npmjs.com/package/oco-claude-plugin)
|
|
5
|
+
[](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
|
|
7
|
-
* npx
|
|
8
|
-
* npx
|
|
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
|
|
236
|
-
npx
|
|
237
|
-
npx
|
|
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.
|
|
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
|
+
}
|
package/plugin/mcp/bridge.js
CHANGED
|
@@ -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
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
for
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
"
|
|
227
|
-
|
|
228
|
-
"--
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
//
|
|
337
|
-
const
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
if (hasFile("
|
|
388
|
-
return { command: "
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
return null;
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
});
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
function
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
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
|
+
}
|