@conduit-ai/mcp-server 0.0.2 → 0.0.3
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/dist/index.js +170 -2
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -28399,7 +28399,8 @@ function formatError2(status, data) {
|
|
|
28399
28399
|
function openCodePluginJS() {
|
|
28400
28400
|
return `// Conduit OpenCode plugin — auto-generated by @conduit-ai/mcp-server
|
|
28401
28401
|
// Forwards OpenCode session/message events to the Conduit API.
|
|
28402
|
-
//
|
|
28402
|
+
// Syncs the local config file to/from the Conduit dashboard.
|
|
28403
|
+
// Injects pending prompts from the dashboard into the active agent session.
|
|
28403
28404
|
const TOKEN = ${JSON.stringify(HOOK_TOKEN)};
|
|
28404
28405
|
const API_URL = ${JSON.stringify(API_URL)};
|
|
28405
28406
|
|
|
@@ -28492,10 +28493,171 @@ function extractSessionId(type, props) {
|
|
|
28492
28493
|
}
|
|
28493
28494
|
}
|
|
28494
28495
|
|
|
28495
|
-
|
|
28496
|
+
// ---------------------------------------------------------------------------
|
|
28497
|
+
// Prompt injection: polls Conduit for pending prompts and injects them
|
|
28498
|
+
// into the active OpenCode session via client.session.promptAsync().
|
|
28499
|
+
// ---------------------------------------------------------------------------
|
|
28500
|
+
|
|
28501
|
+
let _injecting = false; // guard against concurrent injection
|
|
28502
|
+
|
|
28503
|
+
/**
|
|
28504
|
+
* Find the best session to inject a prompt into.
|
|
28505
|
+
* Priority: 1) explicit sessionId from the prompt, 2) idle root session, 3) any root session.
|
|
28506
|
+
* Subagent sessions (those with parentID) are always excluded.
|
|
28507
|
+
*/
|
|
28508
|
+
async function findTargetSession(client, preferredId) {
|
|
28509
|
+
// If the dashboard specified a session, use it directly
|
|
28510
|
+
if (preferredId && preferredId !== "unknown") return preferredId;
|
|
28511
|
+
|
|
28512
|
+
try {
|
|
28513
|
+
const sessResult = await client.session.list();
|
|
28514
|
+
const sessions = sessResult?.data;
|
|
28515
|
+
if (!Array.isArray(sessions) || sessions.length === 0) return null;
|
|
28516
|
+
|
|
28517
|
+
// Filter out subagent sessions (those with parentID)
|
|
28518
|
+
const rootSessions = sessions.filter((s) => !s.parentID);
|
|
28519
|
+
if (rootSessions.length === 0) return null;
|
|
28520
|
+
|
|
28521
|
+
// Check which sessions are idle (absent from status map = idle)
|
|
28522
|
+
try {
|
|
28523
|
+
const statusResult = await client.session.status();
|
|
28524
|
+
const statuses = statusResult?.data;
|
|
28525
|
+
if (statuses && typeof statuses === "object") {
|
|
28526
|
+
// Find the first root session that is idle (not in the busy/retry map)
|
|
28527
|
+
const idleSession = rootSessions.find((s) => !statuses[s.id]);
|
|
28528
|
+
if (idleSession) return idleSession.id;
|
|
28529
|
+
}
|
|
28530
|
+
} catch (_) { /* status endpoint failed, fall through */ }
|
|
28531
|
+
|
|
28532
|
+
// Fallback: use the most recent root session regardless of status
|
|
28533
|
+
// (promptAsync queues internally if the session is busy)
|
|
28534
|
+
return rootSessions[0].id;
|
|
28535
|
+
} catch (_) { return null; }
|
|
28536
|
+
}
|
|
28537
|
+
|
|
28538
|
+
async function injectPendingPrompts(client, idleSessionId) {
|
|
28539
|
+
if (_injecting || !client) return;
|
|
28540
|
+
_injecting = true;
|
|
28541
|
+
try {
|
|
28542
|
+
const resp = await fetch(\`\${API_URL}/api/prompts/pending\`, {
|
|
28543
|
+
headers: { "Authorization": \`Bearer \${TOKEN}\` },
|
|
28544
|
+
});
|
|
28545
|
+
if (!resp.ok) return;
|
|
28546
|
+
const body = await resp.json();
|
|
28547
|
+
const prompts = body?.data;
|
|
28548
|
+
if (!Array.isArray(prompts) || prompts.length === 0) return;
|
|
28549
|
+
|
|
28550
|
+
for (const prompt of prompts) {
|
|
28551
|
+
// Resolve target: prompt's own sessionId > idleSessionId from event > auto-discover
|
|
28552
|
+
const targetId = await findTargetSession(
|
|
28553
|
+
client,
|
|
28554
|
+
(prompt.sessionId && prompt.sessionId !== "unknown") ? prompt.sessionId : idleSessionId,
|
|
28555
|
+
);
|
|
28556
|
+
if (!targetId) continue;
|
|
28557
|
+
|
|
28558
|
+
try {
|
|
28559
|
+
await client.session.promptAsync({
|
|
28560
|
+
path: { id: targetId },
|
|
28561
|
+
body: {
|
|
28562
|
+
parts: [{ type: "text", text: prompt.content }],
|
|
28563
|
+
},
|
|
28564
|
+
});
|
|
28565
|
+
|
|
28566
|
+
// ACK the prompt as delivered
|
|
28567
|
+
await fetch(\`\${API_URL}/api/prompts/\${prompt.id}/ack\`, {
|
|
28568
|
+
method: "POST",
|
|
28569
|
+
headers: {
|
|
28570
|
+
"Authorization": \`Bearer \${TOKEN}\`,
|
|
28571
|
+
"Content-Type": "application/json",
|
|
28572
|
+
},
|
|
28573
|
+
body: JSON.stringify({ status: "delivered" }),
|
|
28574
|
+
});
|
|
28575
|
+
} catch (err) {
|
|
28576
|
+
// ACK as failed so it doesn't get retried forever
|
|
28577
|
+
try {
|
|
28578
|
+
await fetch(\`\${API_URL}/api/prompts/\${prompt.id}/ack\`, {
|
|
28579
|
+
method: "POST",
|
|
28580
|
+
headers: {
|
|
28581
|
+
"Authorization": \`Bearer \${TOKEN}\`,
|
|
28582
|
+
"Content-Type": "application/json",
|
|
28583
|
+
},
|
|
28584
|
+
body: JSON.stringify({ status: "failed", error: String(err) }),
|
|
28585
|
+
});
|
|
28586
|
+
} catch (_) { /* best-effort */ }
|
|
28587
|
+
}
|
|
28588
|
+
}
|
|
28589
|
+
} catch (_) { /* best-effort */ }
|
|
28590
|
+
finally { _injecting = false; }
|
|
28591
|
+
}
|
|
28592
|
+
|
|
28593
|
+
// ---------------------------------------------------------------------------
|
|
28594
|
+
// SSE prompt stream: real-time prompt delivery from the Conduit API.
|
|
28595
|
+
// When a prompt is queued on the dashboard, the SSE stream pushes it here
|
|
28596
|
+
// immediately instead of waiting for the next session.idle poll.
|
|
28597
|
+
// ---------------------------------------------------------------------------
|
|
28598
|
+
|
|
28599
|
+
function startPromptStream(client) {
|
|
28600
|
+
if (!client) return;
|
|
28601
|
+
let backoff = 1000;
|
|
28602
|
+
const MAX_BACKOFF = 30000;
|
|
28603
|
+
let stopped = false;
|
|
28604
|
+
|
|
28605
|
+
async function connect() {
|
|
28606
|
+
if (stopped) return;
|
|
28607
|
+
try {
|
|
28608
|
+
const res = await fetch(\`\${API_URL}/api/prompts/stream\`, {
|
|
28609
|
+
headers: { "Authorization": \`Bearer \${TOKEN}\` },
|
|
28610
|
+
});
|
|
28611
|
+
if (!res.ok || !res.body) throw new Error("HTTP " + res.status);
|
|
28612
|
+
backoff = 1000;
|
|
28613
|
+
|
|
28614
|
+
const decoder = new TextDecoder();
|
|
28615
|
+
let buffer = "";
|
|
28616
|
+
const reader = res.body.getReader();
|
|
28617
|
+
|
|
28618
|
+
while (true) {
|
|
28619
|
+
const { done, value } = await reader.read();
|
|
28620
|
+
if (done) break;
|
|
28621
|
+
buffer += decoder.decode(value, { stream: true });
|
|
28622
|
+
const frames = buffer.split("\\n\\n");
|
|
28623
|
+
buffer = frames.pop() ?? "";
|
|
28624
|
+
|
|
28625
|
+
for (const frame of frames) {
|
|
28626
|
+
if (!frame.trim()) continue;
|
|
28627
|
+
let eventType = "";
|
|
28628
|
+
let data = "";
|
|
28629
|
+
for (const line of frame.split("\\n")) {
|
|
28630
|
+
if (line.startsWith("event: ")) eventType = line.slice(7);
|
|
28631
|
+
else if (line.startsWith("data: ")) data = line.slice(6);
|
|
28632
|
+
}
|
|
28633
|
+
if (eventType === "prompt.queued" && data) {
|
|
28634
|
+
// A prompt just arrived — inject it immediately
|
|
28635
|
+
await injectPendingPrompts(client, null);
|
|
28636
|
+
}
|
|
28637
|
+
}
|
|
28638
|
+
}
|
|
28639
|
+
} catch (_) { /* reconnect */ }
|
|
28640
|
+
|
|
28641
|
+
if (!stopped) {
|
|
28642
|
+
setTimeout(() => connect(), backoff);
|
|
28643
|
+
backoff = Math.min(backoff * 2, MAX_BACKOFF);
|
|
28644
|
+
}
|
|
28645
|
+
}
|
|
28646
|
+
|
|
28647
|
+
connect();
|
|
28648
|
+
// No cleanup needed — plugin lifetime = process lifetime
|
|
28649
|
+
}
|
|
28650
|
+
|
|
28651
|
+
export const ConduitPlugin = async ({ client }) => {
|
|
28496
28652
|
await syncConfigToConduit();
|
|
28497
28653
|
await applyPendingConfig();
|
|
28498
28654
|
|
|
28655
|
+
// Start the real-time SSE prompt stream for immediate delivery
|
|
28656
|
+
startPromptStream(client);
|
|
28657
|
+
|
|
28658
|
+
// Also check for any prompts that were queued before the plugin started
|
|
28659
|
+
setTimeout(() => injectPendingPrompts(client, null), 2000);
|
|
28660
|
+
|
|
28499
28661
|
return {
|
|
28500
28662
|
event: async ({ event }) => {
|
|
28501
28663
|
const t = event.type;
|
|
@@ -28508,6 +28670,12 @@ export const ConduitPlugin = async () => {
|
|
|
28508
28670
|
const sessionId = extractSessionId(t, props);
|
|
28509
28671
|
await send(t, sessionId, props);
|
|
28510
28672
|
}
|
|
28673
|
+
|
|
28674
|
+
// On session.idle, check for pending prompts using the now-idle session
|
|
28675
|
+
if (t === "session.idle") {
|
|
28676
|
+
const idleId = event.properties?.sessionID ?? null;
|
|
28677
|
+
await injectPendingPrompts(client, idleId);
|
|
28678
|
+
}
|
|
28511
28679
|
},
|
|
28512
28680
|
};
|
|
28513
28681
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@conduit-ai/mcp-server",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.3",
|
|
4
4
|
"description": "Conduit MCP server — universal AI coding agent integration. Monitor sessions, relay prompts, track metrics from any MCP-compatible client.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|