@minhspark/codex-mcp-bridge 1.10.0 → 1.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +39 -0
- package/LICENSE +21 -21
- package/README.md +16 -2
- package/package.json +2 -2
- package/scripts/check-claude-bridge.mjs +37 -37
- package/scripts/install-launch-agent.mjs +93 -93
- package/scripts/smoke.mjs +50 -50
- package/scripts/sync-version.mjs +17 -10
- package/src/app-server-client.mjs +414 -414
- package/src/claude-bridge.mjs +322 -322
- package/src/index.mjs +44 -15
- package/src/security-policy.mjs +206 -149
- package/src/turn.mjs +140 -140
package/src/turn.mjs
CHANGED
|
@@ -1,140 +1,140 @@
|
|
|
1
|
-
const TERMINAL_STATUSES = new Set(["completed", "interrupted", "failed"]);
|
|
2
|
-
|
|
3
|
-
function summarizeItem(item) {
|
|
4
|
-
switch (item?.type) {
|
|
5
|
-
case "agentMessage":
|
|
6
|
-
return { kind: "agentMessage", text: item.text ?? "" };
|
|
7
|
-
case "commandExecution":
|
|
8
|
-
return {
|
|
9
|
-
kind: "command",
|
|
10
|
-
command: item.command ?? item.parsedCmd ?? null,
|
|
11
|
-
exitCode: item.exitCode ?? null,
|
|
12
|
-
status: item.status ?? null,
|
|
13
|
-
};
|
|
14
|
-
case "fileChange":
|
|
15
|
-
return {
|
|
16
|
-
kind: "fileChange",
|
|
17
|
-
files: (item.changes ?? []).map((c) => c.path ?? c.file ?? null).filter(Boolean),
|
|
18
|
-
status: item.status ?? null,
|
|
19
|
-
};
|
|
20
|
-
case "mcpToolCall":
|
|
21
|
-
return { kind: "mcpToolCall", server: item.server ?? null, tool: item.tool ?? null };
|
|
22
|
-
case "webSearch":
|
|
23
|
-
return { kind: "webSearch", query: item.query ?? null };
|
|
24
|
-
default:
|
|
25
|
-
return null;
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Send one user turn into an existing Codex thread and wait for it to finish.
|
|
31
|
-
* Returns the agent text plus a compact activity trail.
|
|
32
|
-
*/
|
|
33
|
-
export async function runTurn(client, { threadId, input, timeoutMs = 240000, turnOverrides = {} }) {
|
|
34
|
-
const messages = [];
|
|
35
|
-
const activity = [];
|
|
36
|
-
const errors = [];
|
|
37
|
-
const buffered = [];
|
|
38
|
-
let turnId = null;
|
|
39
|
-
let settled = false;
|
|
40
|
-
let resolveDone;
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Only ever resolved, never rejected. `done` has exactly one consumer, and it
|
|
44
|
-
* sits after `turn/start` has already returned - so rejecting it from the
|
|
45
|
-
* catch below would reject a promise nobody is awaiting, which Node turns
|
|
46
|
-
* into an unhandledRejection and, by default, into process exit. A failing
|
|
47
|
-
* `turn/start` (a thread locked by the desktop app is the everyday case) used
|
|
48
|
-
* to take the whole MCP server down that way, while the tool handler was
|
|
49
|
-
* still busy formatting a tidy error message for a client that no longer had
|
|
50
|
-
* a server to talk to.
|
|
51
|
-
*/
|
|
52
|
-
const done = new Promise((resolve) => {
|
|
53
|
-
resolveDone = resolve;
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
const process = (msg) => {
|
|
57
|
-
const params = msg.params ?? {};
|
|
58
|
-
if (turnId && params.turnId && params.turnId !== turnId) return;
|
|
59
|
-
|
|
60
|
-
switch (msg.method) {
|
|
61
|
-
case "item/completed": {
|
|
62
|
-
const summary = summarizeItem(params.item);
|
|
63
|
-
if (!summary) return;
|
|
64
|
-
if (summary.kind === "agentMessage") {
|
|
65
|
-
if (summary.text.trim()) messages.push(summary.text);
|
|
66
|
-
} else {
|
|
67
|
-
activity.push(summary);
|
|
68
|
-
}
|
|
69
|
-
return;
|
|
70
|
-
}
|
|
71
|
-
case "error": {
|
|
72
|
-
errors.push(params.error ?? { message: "unknown error" });
|
|
73
|
-
if (!params.willRetry && !settled) {
|
|
74
|
-
settled = true;
|
|
75
|
-
resolveDone({ status: "failed", error: params.error ?? null });
|
|
76
|
-
}
|
|
77
|
-
return;
|
|
78
|
-
}
|
|
79
|
-
case "turn/completed": {
|
|
80
|
-
const turn = params.turn ?? {};
|
|
81
|
-
if (turnId && turn.id && turn.id !== turnId) return;
|
|
82
|
-
if (!TERMINAL_STATUSES.has(turn.status)) return;
|
|
83
|
-
if (settled) return;
|
|
84
|
-
settled = true;
|
|
85
|
-
resolveDone({ status: turn.status, error: turn.error ?? null, durationMs: turn.durationMs ?? null });
|
|
86
|
-
return;
|
|
87
|
-
}
|
|
88
|
-
default:
|
|
89
|
-
}
|
|
90
|
-
};
|
|
91
|
-
|
|
92
|
-
const unsubscribe = client.subscribe(threadId, (msg) => {
|
|
93
|
-
if (!turnId) {
|
|
94
|
-
buffered.push(msg);
|
|
95
|
-
return;
|
|
96
|
-
}
|
|
97
|
-
process(msg);
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
const timer = globalThis.setTimeout(() => {
|
|
101
|
-
if (settled) return;
|
|
102
|
-
settled = true;
|
|
103
|
-
resolveDone({ status: "timeout", error: null });
|
|
104
|
-
}, timeoutMs);
|
|
105
|
-
|
|
106
|
-
const unsubscribeDisconnect = client.subscribeDisconnect(() => {
|
|
107
|
-
if (settled) return;
|
|
108
|
-
settled = true;
|
|
109
|
-
resolveDone({
|
|
110
|
-
status: "disconnected",
|
|
111
|
-
error: { message: "the app-server connection dropped while the turn was running" },
|
|
112
|
-
});
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
try {
|
|
116
|
-
const started = await client.request(
|
|
117
|
-
"turn/start",
|
|
118
|
-
{ threadId, input, ...turnOverrides },
|
|
119
|
-
{ timeoutMs: Math.min(timeoutMs, 60000) },
|
|
120
|
-
);
|
|
121
|
-
turnId = started?.turn?.id ?? null;
|
|
122
|
-
for (const msg of buffered.splice(0)) process(msg);
|
|
123
|
-
|
|
124
|
-
const outcome = await done;
|
|
125
|
-
return {
|
|
126
|
-
threadId,
|
|
127
|
-
turnId,
|
|
128
|
-
status: outcome.status,
|
|
129
|
-
error: outcome.error,
|
|
130
|
-
durationMs: outcome.durationMs ?? null,
|
|
131
|
-
text: messages.join("\n\n").trim(),
|
|
132
|
-
activity,
|
|
133
|
-
errors,
|
|
134
|
-
};
|
|
135
|
-
} finally {
|
|
136
|
-
globalThis.clearTimeout(timer);
|
|
137
|
-
unsubscribe();
|
|
138
|
-
unsubscribeDisconnect();
|
|
139
|
-
}
|
|
140
|
-
}
|
|
1
|
+
const TERMINAL_STATUSES = new Set(["completed", "interrupted", "failed"]);
|
|
2
|
+
|
|
3
|
+
function summarizeItem(item) {
|
|
4
|
+
switch (item?.type) {
|
|
5
|
+
case "agentMessage":
|
|
6
|
+
return { kind: "agentMessage", text: item.text ?? "" };
|
|
7
|
+
case "commandExecution":
|
|
8
|
+
return {
|
|
9
|
+
kind: "command",
|
|
10
|
+
command: item.command ?? item.parsedCmd ?? null,
|
|
11
|
+
exitCode: item.exitCode ?? null,
|
|
12
|
+
status: item.status ?? null,
|
|
13
|
+
};
|
|
14
|
+
case "fileChange":
|
|
15
|
+
return {
|
|
16
|
+
kind: "fileChange",
|
|
17
|
+
files: (item.changes ?? []).map((c) => c.path ?? c.file ?? null).filter(Boolean),
|
|
18
|
+
status: item.status ?? null,
|
|
19
|
+
};
|
|
20
|
+
case "mcpToolCall":
|
|
21
|
+
return { kind: "mcpToolCall", server: item.server ?? null, tool: item.tool ?? null };
|
|
22
|
+
case "webSearch":
|
|
23
|
+
return { kind: "webSearch", query: item.query ?? null };
|
|
24
|
+
default:
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Send one user turn into an existing Codex thread and wait for it to finish.
|
|
31
|
+
* Returns the agent text plus a compact activity trail.
|
|
32
|
+
*/
|
|
33
|
+
export async function runTurn(client, { threadId, input, timeoutMs = 240000, turnOverrides = {} }) {
|
|
34
|
+
const messages = [];
|
|
35
|
+
const activity = [];
|
|
36
|
+
const errors = [];
|
|
37
|
+
const buffered = [];
|
|
38
|
+
let turnId = null;
|
|
39
|
+
let settled = false;
|
|
40
|
+
let resolveDone;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Only ever resolved, never rejected. `done` has exactly one consumer, and it
|
|
44
|
+
* sits after `turn/start` has already returned - so rejecting it from the
|
|
45
|
+
* catch below would reject a promise nobody is awaiting, which Node turns
|
|
46
|
+
* into an unhandledRejection and, by default, into process exit. A failing
|
|
47
|
+
* `turn/start` (a thread locked by the desktop app is the everyday case) used
|
|
48
|
+
* to take the whole MCP server down that way, while the tool handler was
|
|
49
|
+
* still busy formatting a tidy error message for a client that no longer had
|
|
50
|
+
* a server to talk to.
|
|
51
|
+
*/
|
|
52
|
+
const done = new Promise((resolve) => {
|
|
53
|
+
resolveDone = resolve;
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const process = (msg) => {
|
|
57
|
+
const params = msg.params ?? {};
|
|
58
|
+
if (turnId && params.turnId && params.turnId !== turnId) return;
|
|
59
|
+
|
|
60
|
+
switch (msg.method) {
|
|
61
|
+
case "item/completed": {
|
|
62
|
+
const summary = summarizeItem(params.item);
|
|
63
|
+
if (!summary) return;
|
|
64
|
+
if (summary.kind === "agentMessage") {
|
|
65
|
+
if (summary.text.trim()) messages.push(summary.text);
|
|
66
|
+
} else {
|
|
67
|
+
activity.push(summary);
|
|
68
|
+
}
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
case "error": {
|
|
72
|
+
errors.push(params.error ?? { message: "unknown error" });
|
|
73
|
+
if (!params.willRetry && !settled) {
|
|
74
|
+
settled = true;
|
|
75
|
+
resolveDone({ status: "failed", error: params.error ?? null });
|
|
76
|
+
}
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
case "turn/completed": {
|
|
80
|
+
const turn = params.turn ?? {};
|
|
81
|
+
if (turnId && turn.id && turn.id !== turnId) return;
|
|
82
|
+
if (!TERMINAL_STATUSES.has(turn.status)) return;
|
|
83
|
+
if (settled) return;
|
|
84
|
+
settled = true;
|
|
85
|
+
resolveDone({ status: turn.status, error: turn.error ?? null, durationMs: turn.durationMs ?? null });
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
default:
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const unsubscribe = client.subscribe(threadId, (msg) => {
|
|
93
|
+
if (!turnId) {
|
|
94
|
+
buffered.push(msg);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
process(msg);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
const timer = globalThis.setTimeout(() => {
|
|
101
|
+
if (settled) return;
|
|
102
|
+
settled = true;
|
|
103
|
+
resolveDone({ status: "timeout", error: null });
|
|
104
|
+
}, timeoutMs);
|
|
105
|
+
|
|
106
|
+
const unsubscribeDisconnect = client.subscribeDisconnect(() => {
|
|
107
|
+
if (settled) return;
|
|
108
|
+
settled = true;
|
|
109
|
+
resolveDone({
|
|
110
|
+
status: "disconnected",
|
|
111
|
+
error: { message: "the app-server connection dropped while the turn was running" },
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
const started = await client.request(
|
|
117
|
+
"turn/start",
|
|
118
|
+
{ threadId, input, ...turnOverrides },
|
|
119
|
+
{ timeoutMs: Math.min(timeoutMs, 60000) },
|
|
120
|
+
);
|
|
121
|
+
turnId = started?.turn?.id ?? null;
|
|
122
|
+
for (const msg of buffered.splice(0)) process(msg);
|
|
123
|
+
|
|
124
|
+
const outcome = await done;
|
|
125
|
+
return {
|
|
126
|
+
threadId,
|
|
127
|
+
turnId,
|
|
128
|
+
status: outcome.status,
|
|
129
|
+
error: outcome.error,
|
|
130
|
+
durationMs: outcome.durationMs ?? null,
|
|
131
|
+
text: messages.join("\n\n").trim(),
|
|
132
|
+
activity,
|
|
133
|
+
errors,
|
|
134
|
+
};
|
|
135
|
+
} finally {
|
|
136
|
+
globalThis.clearTimeout(timer);
|
|
137
|
+
unsubscribe();
|
|
138
|
+
unsubscribeDisconnect();
|
|
139
|
+
}
|
|
140
|
+
}
|