@heretyc/subagent-mcp 2.6.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/LICENSE +201 -0
- package/NOTICE +5 -0
- package/README.md +124 -0
- package/directives/carryover-claude.md +17 -0
- package/directives/carryover-codex.md +17 -0
- package/directives/off-turn-reminder.md +1 -0
- package/directives/orchestration-claude.md +21 -0
- package/directives/orchestration-codex.md +22 -0
- package/dist/advanced-ruleset.py +67 -0
- package/dist/deadlock.js +8 -0
- package/dist/doctor.js +32 -0
- package/dist/effort.js +78 -0
- package/dist/hooks/orchestration-claude.js +88 -0
- package/dist/hooks/orchestration-codex.js +152 -0
- package/dist/index.js +908 -0
- package/dist/orchestration/hook-core.js +208 -0
- package/dist/orchestration/marker.js +139 -0
- package/dist/output-helpers.js +128 -0
- package/dist/platform.js +59 -0
- package/dist/routing-table.json +3821 -0
- package/dist/routing.js +260 -0
- package/dist/ruleset-scaffold.js +2 -0
- package/dist/ruleset.js +319 -0
- package/dist/setup.js +507 -0
- package/dist/status-helpers.js +56 -0
- package/dist/stream-helpers.js +182 -0
- package/dist/wait-helpers.js +21 -0
- package/package.json +51 -0
- package/scripts/postinstall.mjs +102 -0
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
// Defensive parsing of a sub-agent's VISIBLE provider stream.
|
|
2
|
+
//
|
|
3
|
+
// "Visible" = provider stream events, summaries, and assistant messages a human
|
|
4
|
+
// would see in the CLI. Provider-internal reasoning blocks (Claude `thinking` /
|
|
5
|
+
// `redacted_thinking` content blocks, Codex `reasoning` items/events) are not
|
|
6
|
+
// parsed into visible items. Supports Codex `--json` JSONL and Claude
|
|
7
|
+
// stream-json / buffered-json where feasible. NEVER throws; unparseable lines
|
|
8
|
+
// are skipped.
|
|
9
|
+
function pushText(out, type, text) {
|
|
10
|
+
if (typeof text === "string" && text.trim().length > 0) {
|
|
11
|
+
out.push({ type, text: text.trim() });
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
// Codex `exec --json` is newline-delimited JSON. Visible: agent messages and
|
|
15
|
+
// completed items that carry text. Chain-of-thought (anything whose type or
|
|
16
|
+
// item_type is `reasoning`) is dropped.
|
|
17
|
+
function collectCodex(e, out) {
|
|
18
|
+
const type = typeof e.type === "string" ? e.type : "";
|
|
19
|
+
if (type.includes("reasoning"))
|
|
20
|
+
return;
|
|
21
|
+
// Shape A: { type: "agent_message", message: "..." }
|
|
22
|
+
if (type === "agent_message") {
|
|
23
|
+
pushText(out, "agent_message", e.message);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
// Shape B: { type: "item.completed", item: { item_type, text } }
|
|
27
|
+
if (type === "item.completed" && e.item && typeof e.item === "object") {
|
|
28
|
+
const item = e.item;
|
|
29
|
+
const itemType = typeof item.item_type === "string" ? item.item_type : "item";
|
|
30
|
+
if (itemType.includes("reasoning"))
|
|
31
|
+
return;
|
|
32
|
+
pushText(out, itemType, item.text);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
// Shape C: { msg: { type: "agent_message", message: "..." } }
|
|
36
|
+
if (e.msg && typeof e.msg === "object") {
|
|
37
|
+
const msg = e.msg;
|
|
38
|
+
const msgType = typeof msg.type === "string" ? msg.type : "";
|
|
39
|
+
if (msgType.includes("reasoning"))
|
|
40
|
+
return;
|
|
41
|
+
if (msgType === "agent_message")
|
|
42
|
+
pushText(out, "agent_message", msg.message);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
// Claude stream-json emits one JSON event per line. Visible: assistant `text`
|
|
46
|
+
// and `tool_use` blocks plus the final `result`. `thinking` /
|
|
47
|
+
// `redacted_thinking` blocks are dropped. Tolerates the buffered-json single
|
|
48
|
+
// `result` object too.
|
|
49
|
+
function collectClaude(e, out) {
|
|
50
|
+
if (e.type === "result") {
|
|
51
|
+
pushText(out, "result", e.result);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
if (e.type === "assistant" && e.message && typeof e.message === "object") {
|
|
55
|
+
const message = e.message;
|
|
56
|
+
const content = message.content;
|
|
57
|
+
if (!Array.isArray(content))
|
|
58
|
+
return;
|
|
59
|
+
for (const block of content) {
|
|
60
|
+
if (!block || typeof block !== "object")
|
|
61
|
+
continue;
|
|
62
|
+
const b = block;
|
|
63
|
+
if (b.type === "text")
|
|
64
|
+
pushText(out, "text", b.text);
|
|
65
|
+
else if (b.type === "tool_use")
|
|
66
|
+
pushText(out, "tool_use", b.name);
|
|
67
|
+
// thinking / redacted_thinking: provider-internal reasoning, not parsed as visible.
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
// Returns true if a complete JSONL line contains only provider-internal reasoning
|
|
72
|
+
// (Claude `thinking`/`redacted_thinking` blocks, Codex `reasoning` items) —
|
|
73
|
+
// records that produce no visible stream items. Never throws.
|
|
74
|
+
export function isNonVisibleStreamLine(provider, line) {
|
|
75
|
+
const trimmed = line.trim();
|
|
76
|
+
if (!trimmed)
|
|
77
|
+
return false;
|
|
78
|
+
let evt;
|
|
79
|
+
try {
|
|
80
|
+
evt = JSON.parse(trimmed);
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
if (!evt || typeof evt !== "object")
|
|
86
|
+
return false;
|
|
87
|
+
const e = evt;
|
|
88
|
+
if (provider === "codex") {
|
|
89
|
+
const type = typeof e.type === "string" ? e.type : "";
|
|
90
|
+
if (type.includes("reasoning"))
|
|
91
|
+
return true;
|
|
92
|
+
if (type === "item.completed" && e.item && typeof e.item === "object") {
|
|
93
|
+
const item = e.item;
|
|
94
|
+
if (typeof item.item_type === "string" && item.item_type.includes("reasoning"))
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
if (e.msg && typeof e.msg === "object") {
|
|
98
|
+
const msg = e.msg;
|
|
99
|
+
if (typeof msg.type === "string" && msg.type.includes("reasoning"))
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
if (provider === "claude") {
|
|
105
|
+
if (e.type !== "assistant")
|
|
106
|
+
return false;
|
|
107
|
+
if (!e.message || typeof e.message !== "object")
|
|
108
|
+
return false;
|
|
109
|
+
const content = e.message.content;
|
|
110
|
+
if (!Array.isArray(content) || content.length === 0)
|
|
111
|
+
return false;
|
|
112
|
+
// Only pure hidden if every block is thinking/redacted_thinking.
|
|
113
|
+
return content.every((block) => {
|
|
114
|
+
if (!block || typeof block !== "object")
|
|
115
|
+
return false;
|
|
116
|
+
const b = block;
|
|
117
|
+
return b.type === "thinking" || b.type === "redacted_thinking";
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
// Parse a SINGLE complete JSONL line into visible items (appended to `out`).
|
|
123
|
+
// Blank/unparseable lines are skipped. Never throws.
|
|
124
|
+
function collectLine(provider, line, out) {
|
|
125
|
+
const trimmed = line.trim();
|
|
126
|
+
if (!trimmed)
|
|
127
|
+
return;
|
|
128
|
+
let evt;
|
|
129
|
+
try {
|
|
130
|
+
evt = JSON.parse(trimmed);
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
if (!evt || typeof evt !== "object")
|
|
136
|
+
return;
|
|
137
|
+
if (provider === "codex")
|
|
138
|
+
collectCodex(evt, out);
|
|
139
|
+
else if (provider === "claude")
|
|
140
|
+
collectClaude(evt, out);
|
|
141
|
+
}
|
|
142
|
+
export function parseVisibleStream(provider, chunk) {
|
|
143
|
+
const out = [];
|
|
144
|
+
if (!chunk)
|
|
145
|
+
return out;
|
|
146
|
+
for (const line of chunk.split("\n"))
|
|
147
|
+
collectLine(provider, line, out);
|
|
148
|
+
return out;
|
|
149
|
+
}
|
|
150
|
+
export function consumeStreamChunk(provider, pending, chunk) {
|
|
151
|
+
const data = (pending || "") + (chunk || "");
|
|
152
|
+
const parts = data.split("\n");
|
|
153
|
+
// The last element is the trailing fragment: incomplete unless the chunk ended
|
|
154
|
+
// with a newline (in which case it is "").
|
|
155
|
+
const remainder = parts.pop() ?? "";
|
|
156
|
+
const items = [];
|
|
157
|
+
const lines = [];
|
|
158
|
+
for (const line of parts) {
|
|
159
|
+
if (!line.trim())
|
|
160
|
+
continue;
|
|
161
|
+
lines.push(line);
|
|
162
|
+
collectLine(provider, line, items);
|
|
163
|
+
}
|
|
164
|
+
return { items, pending: remainder, lines };
|
|
165
|
+
}
|
|
166
|
+
// Flush any buffered trailing fragment (call once on stream close, where the
|
|
167
|
+
// final line may have arrived without a terminating newline).
|
|
168
|
+
export function flushStream(provider, pending) {
|
|
169
|
+
const trimmed = (pending || "").trim();
|
|
170
|
+
if (!trimmed)
|
|
171
|
+
return { items: [], pending: "", lines: [] };
|
|
172
|
+
const items = [];
|
|
173
|
+
collectLine(provider, trimmed, items);
|
|
174
|
+
return { items, pending: "", lines: [trimmed] };
|
|
175
|
+
}
|
|
176
|
+
// Append new items to a rolling buffer, retaining only the last `n`.
|
|
177
|
+
export function retainLastN(buffer, items, n) {
|
|
178
|
+
if (items.length === 0)
|
|
179
|
+
return buffer;
|
|
180
|
+
const merged = buffer.concat(items);
|
|
181
|
+
return merged.length > n ? merged.slice(-n) : merged;
|
|
182
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export function formatLocalIso(ms) {
|
|
2
|
+
const d = new Date(ms);
|
|
3
|
+
const pad2 = (n) => String(n).padStart(2, "0");
|
|
4
|
+
const year = d.getFullYear();
|
|
5
|
+
const month = pad2(d.getMonth() + 1);
|
|
6
|
+
const day = pad2(d.getDate());
|
|
7
|
+
const hours = pad2(d.getHours());
|
|
8
|
+
const minutes = pad2(d.getMinutes());
|
|
9
|
+
const seconds = pad2(d.getSeconds());
|
|
10
|
+
const offsetMin = -d.getTimezoneOffset();
|
|
11
|
+
const sign = offsetMin >= 0 ? "+" : "-";
|
|
12
|
+
const absMin = Math.abs(offsetMin);
|
|
13
|
+
const offH = pad2(Math.floor(absMin / 60));
|
|
14
|
+
const offM = pad2(absMin % 60);
|
|
15
|
+
const zone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
16
|
+
return `${year}-${month}-${day}T${hours}:${minutes}:${seconds}${sign}${offH}:${offM} (${zone})`;
|
|
17
|
+
}
|
|
18
|
+
const TERMINAL_STATUSES = new Set(["finished", "errored", "stopped"]);
|
|
19
|
+
export function selectUnreported(list) {
|
|
20
|
+
return list.filter((a) => TERMINAL_STATUSES.has(a.status) && a.exitedAt !== null && !a.waitReported);
|
|
21
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@heretyc/subagent-mcp",
|
|
3
|
+
"version": "2.6.0",
|
|
4
|
+
"description": "MCP server that launches and manages local Claude Code and Codex CLI sub-agents as child processes (no direct Anthropic/OpenAI API).",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"subagent-mcp": "dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"directives",
|
|
13
|
+
"scripts/postinstall.mjs",
|
|
14
|
+
"LICENSE",
|
|
15
|
+
"NOTICE",
|
|
16
|
+
"README.md"
|
|
17
|
+
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "node scripts/gen-ruleset-scaffold.mjs && tsc && node scripts/copy-provider.mjs",
|
|
20
|
+
"start": "node dist/index.js",
|
|
21
|
+
"postinstall": "node scripts/postinstall.mjs",
|
|
22
|
+
"prepare": "npm run build",
|
|
23
|
+
"prepublishOnly": "npm test",
|
|
24
|
+
"test": "node test/effort.test.mjs && node test/platform.test.mjs && node test/wait.test.mjs && node test/status.test.mjs && node test/output.test.mjs && node test/stream.test.mjs && node test/routing.test.mjs && node test/deadlock.test.mjs && node test/handler-validation.test.mjs && node test/index-handler.test.mjs && node test/ruleset.test.mjs && node test/ruleset-exec.test.mjs && node test/ruleset-handler.test.mjs && node test/failover.test.mjs && node test/orchestration-marker.test.mjs && node test/orchestration-hook-core.test.mjs && node test/orchestration-adapters.test.mjs && node test/orchestration-directives.test.mjs && node test/setup-repair.test.mjs && node scripts/validate_provider.mjs && node scripts/validate_seed_sites.mjs && node scripts/validate_routing_audit.mjs && node test/seed-sites.test.mjs && node test/mcp-compliance.test.mjs"
|
|
25
|
+
},
|
|
26
|
+
"author": "Lexi Blackburn",
|
|
27
|
+
"license": "Apache-2.0",
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "git+https://github.com/Heretyc/subagent-mcp.git"
|
|
31
|
+
},
|
|
32
|
+
"homepage": "https://github.com/Heretyc/subagent-mcp#readme",
|
|
33
|
+
"bugs": {
|
|
34
|
+
"url": "https://github.com/Heretyc/subagent-mcp/issues"
|
|
35
|
+
},
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
38
|
+
"zod": "^3.0.0"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@types/node": "^20.0.0",
|
|
42
|
+
"typescript": "^5.0.0"
|
|
43
|
+
},
|
|
44
|
+
"engines": {
|
|
45
|
+
"node": ">=18"
|
|
46
|
+
},
|
|
47
|
+
"publishConfig": {
|
|
48
|
+
"registry": "https://npm.pkg.github.com",
|
|
49
|
+
"access": "public"
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Postinstall banner for subagent-mcp.
|
|
3
|
+
//
|
|
4
|
+
// `npm install -g @heretyc/subagent-mcp` ships only the MCP server + hook
|
|
5
|
+
// assets; it does NOT wire them into Claude Code / Codex. Without feedback the
|
|
6
|
+
// user has no idea an addon landed, let alone that a second step is required.
|
|
7
|
+
// This prints a clear "what installed / what to run next / how to verify"
|
|
8
|
+
// banner so the install is self-explanatory.
|
|
9
|
+
//
|
|
10
|
+
// Rules:
|
|
11
|
+
// - NEVER fail the install. Any error is swallowed; always exit 0.
|
|
12
|
+
// - Only speak for a real end-user install. In the dev checkout (src/ present)
|
|
13
|
+
// stay silent so `npm install` during development isn't noisy.
|
|
14
|
+
// - Print only — do not mutate vendor config. Wiring is the explicit,
|
|
15
|
+
// reversible `subagent-mcp setup` step.
|
|
16
|
+
|
|
17
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
18
|
+
import { homedir } from "node:os";
|
|
19
|
+
import { join, dirname, resolve } from "node:path";
|
|
20
|
+
import { fileURLToPath } from "node:url";
|
|
21
|
+
import { execSync } from "node:child_process";
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
// Install root: scripts/postinstall.mjs -> scripts/ -> <root>
|
|
25
|
+
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
26
|
+
|
|
27
|
+
// Dev checkout? (src/ only exists in the repo, never in the shipped tarball.)
|
|
28
|
+
// Stay silent there — the maintainer doesn't need the end-user banner.
|
|
29
|
+
if (existsSync(join(ROOT, "src"))) process.exit(0);
|
|
30
|
+
|
|
31
|
+
let version = "";
|
|
32
|
+
try {
|
|
33
|
+
version = JSON.parse(readFileSync(join(ROOT, "package.json"), "utf8")).version || "";
|
|
34
|
+
} catch { /* version is cosmetic */ }
|
|
35
|
+
|
|
36
|
+
// Best-effort vendor detection so the banner can say what WILL be wired.
|
|
37
|
+
// Pure read-only; failures just fall back to "not detected".
|
|
38
|
+
function has(cmd) {
|
|
39
|
+
try {
|
|
40
|
+
execSync(process.platform === "win32" ? `where ${cmd}` : `command -v ${cmd}`, {
|
|
41
|
+
stdio: "ignore",
|
|
42
|
+
});
|
|
43
|
+
return true;
|
|
44
|
+
} catch {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
const hasClaude = has("claude");
|
|
49
|
+
const hasCodex = has("codex") || existsSync(join(homedir(), ".codex"));
|
|
50
|
+
|
|
51
|
+
const L = [];
|
|
52
|
+
const line = (s = "") => L.push(s);
|
|
53
|
+
const bar = "============================================================";
|
|
54
|
+
|
|
55
|
+
line();
|
|
56
|
+
line(bar);
|
|
57
|
+
line(` subagent-mcp installed${version ? ` (v${version})` : ""}`);
|
|
58
|
+
line(bar);
|
|
59
|
+
line();
|
|
60
|
+
line(" This is an MCP ADDON for Claude Code CLI and Codex CLI.");
|
|
61
|
+
line(" It is NOT active yet — one command wires it in.");
|
|
62
|
+
line();
|
|
63
|
+
line(" FINISH SETUP (auto-detects vendors, wires all present):");
|
|
64
|
+
line();
|
|
65
|
+
line(" subagent-mcp setup");
|
|
66
|
+
line();
|
|
67
|
+
line(" That registers the MCP server AND installs the per-turn");
|
|
68
|
+
line(" orchestration-mode hooks for every vendor it finds.");
|
|
69
|
+
line();
|
|
70
|
+
|
|
71
|
+
// Detected vendors — concrete, so the user knows what setup will touch.
|
|
72
|
+
if (hasClaude || hasCodex) {
|
|
73
|
+
line(" Detected on this machine:");
|
|
74
|
+
if (hasClaude) line(" - Claude Code CLI (will get MCP server + UserPromptSubmit hook)");
|
|
75
|
+
if (hasCodex) line(" - Codex CLI (will get MCP server + SessionStart/UserPromptSubmit hooks)");
|
|
76
|
+
} else {
|
|
77
|
+
line(" No Claude Code or Codex CLI detected yet. Install one,");
|
|
78
|
+
line(" then run: subagent-mcp setup");
|
|
79
|
+
}
|
|
80
|
+
line();
|
|
81
|
+
|
|
82
|
+
line(" AFTER setup — confirm it took effect:");
|
|
83
|
+
if (hasClaude || !hasCodex) {
|
|
84
|
+
line(" - Claude Code: restart the session, run /mcp");
|
|
85
|
+
line(" -> 'subagent-mcp' shows Connected.");
|
|
86
|
+
}
|
|
87
|
+
if (hasCodex || !hasClaude) {
|
|
88
|
+
line(" - Codex CLI: restart the session, run /hooks");
|
|
89
|
+
line(" -> TRUST the new subagent-mcp hook.");
|
|
90
|
+
}
|
|
91
|
+
line();
|
|
92
|
+
line(" Preview without changes: subagent-mcp setup --dry-run");
|
|
93
|
+
line(" Health check any time: subagent-mcp doctor");
|
|
94
|
+
line(" Docs: https://github.com/Heretyc/subagent-mcp#readme");
|
|
95
|
+
line(bar);
|
|
96
|
+
line();
|
|
97
|
+
|
|
98
|
+
process.stdout.write(L.join("\n") + "\n");
|
|
99
|
+
} catch {
|
|
100
|
+
// Never let a banner failure break the install.
|
|
101
|
+
}
|
|
102
|
+
process.exit(0);
|