@conduit-ai/mcp-server 0.0.2 → 0.0.4

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.
Files changed (2) hide show
  1. package/dist/index.js +187 -4
  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
- // Also syncs the local config file to/from the Conduit dashboard.
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
 
@@ -28453,7 +28454,7 @@ async function applyPendingConfig() {
28453
28454
  const resp = await fetch(\`\${API_URL}/api/config/pending\`, {
28454
28455
  headers: { "Authorization": \`Bearer \${TOKEN}\` },
28455
28456
  });
28456
- if (!resp.ok) return;
28457
+ if (!resp.ok || resp.status === 204) return;
28457
28458
  const body = await resp.json();
28458
28459
  const content = body?.data?.content;
28459
28460
  if (!content) return;
@@ -28492,9 +28493,170 @@ function extractSessionId(type, props) {
28492
28493
  }
28493
28494
  }
28494
28495
 
28495
- export const ConduitPlugin = async () => {
28496
- await syncConfigToConduit();
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 }) => {
28497
28652
  await applyPendingConfig();
28653
+ await syncConfigToConduit();
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);
28498
28660
 
28499
28661
  return {
28500
28662
  event: async ({ event }) => {
@@ -28507,6 +28669,27 @@ export const ConduitPlugin = async () => {
28507
28669
  const props = event.properties ?? {};
28508
28670
  const sessionId = extractSessionId(t, props);
28509
28671
  await send(t, sessionId, props);
28672
+
28673
+ // Detect compaction start from message.part.updated with a compaction part
28674
+ if (t === "message.part.updated") {
28675
+ const part = props.info?.part ?? props.part;
28676
+ if (part && part.type === "compaction") {
28677
+ await send("session.compacting", sessionId, props);
28678
+ }
28679
+ }
28680
+ }
28681
+
28682
+ // Forward session.compacted event (compaction finished)
28683
+ if (t === "session.compacted") {
28684
+ const props = event.properties ?? {};
28685
+ const sessionId = props.sessionID ?? props.sessionId ?? "unknown";
28686
+ await send("session.compacted", sessionId, props);
28687
+ }
28688
+
28689
+ // On session.idle, check for pending prompts using the now-idle session
28690
+ if (t === "session.idle") {
28691
+ const idleId = event.properties?.sessionID ?? null;
28692
+ await injectPendingPrompts(client, idleId);
28510
28693
  }
28511
28694
  },
28512
28695
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@conduit-ai/mcp-server",
3
- "version": "0.0.2",
3
+ "version": "0.0.4",
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": {