@useorgx/openclaw-plugin 0.4.4 → 0.4.5

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.
@@ -9,12 +9,12 @@
9
9
  <meta name="googlebot" content="noindex, nofollow, noarchive, nosnippet, noimageindex" />
10
10
  <meta name="copyright" content="OrgX. All rights reserved." />
11
11
  <title>OrgX Live Dashboard</title>
12
- <script type="module" crossorigin src="/orgx/live/assets/DCBlK4MX.js"></script>
12
+ <script type="module" crossorigin src="/orgx/live/assets/DNjbmawF.js"></script>
13
13
  <link rel="modulepreload" crossorigin href="/orgx/live/assets/C-KIc3Wc.js">
14
- <link rel="modulepreload" crossorigin href="/orgx/live/assets/sAhvFnpk.js">
15
- <link rel="modulepreload" crossorigin href="/orgx/live/assets/B3ziCA02.js">
16
- <link rel="modulepreload" crossorigin href="/orgx/live/assets/4hvaB0UC.js">
17
- <link rel="stylesheet" crossorigin href="/orgx/live/assets/jyFhCND-.css">
14
+ <link rel="modulepreload" crossorigin href="/orgx/live/assets/BZZ-fiJx.js">
15
+ <link rel="modulepreload" crossorigin href="/orgx/live/assets/DD1jv1Hd.js">
16
+ <link rel="modulepreload" crossorigin href="/orgx/live/assets/BoXlCHKa.js">
17
+ <link rel="stylesheet" crossorigin href="/orgx/live/assets/Bq9x_Xyh.css">
18
18
  </head>
19
19
  <body class="bg-[#080808] text-white antialiased">
20
20
  <div id="root"></div>
@@ -2214,13 +2214,22 @@ export function createHttpHandler(config, client, getSnapshot, onboarding, diagn
2214
2214
  type: "progress",
2215
2215
  timestamp,
2216
2216
  payload: {
2217
- phase: input.phase,
2217
+ // Keep this payload aligned with OrgXClient.emitActivity input
2218
+ // so outbox replay can forward it without shape translation.
2219
+ initiative_id: initiativeId,
2220
+ run_id: input.runId?.trim() || undefined,
2221
+ correlation_id: input.runId
2222
+ ? undefined
2223
+ : (input.correlationId?.trim() || `openclaw-${Date.now()}`),
2224
+ source_client: "openclaw",
2218
2225
  message,
2226
+ phase: input.phase,
2227
+ progress_pct: typeof input.progressPct === "number" && Number.isFinite(input.progressPct)
2228
+ ? Math.max(0, Math.min(100, Math.round(input.progressPct)))
2229
+ : undefined,
2219
2230
  level: input.level ?? "info",
2220
- runId,
2221
- initiativeId,
2222
- nextStep: input.nextStep ?? null,
2223
- metadata: input.metadata ?? null,
2231
+ next_step: input.nextStep ?? undefined,
2232
+ metadata: input.metadata ?? undefined,
2224
2233
  },
2225
2234
  activityItem,
2226
2235
  });
package/dist/index.js CHANGED
@@ -22,6 +22,9 @@ import { clearPersistedSnapshot, readPersistedSnapshot, writePersistedSnapshot,
22
22
  import { appendToOutbox, readOutbox, readOutboxSummary, replaceOutbox, } from "./outbox.js";
23
23
  import { extractProgressOutboxMessage } from "./reporting/outbox-replay.js";
24
24
  import { ensureGatewayWatchdog } from "./gateway-watchdog.js";
25
+ import { createMcpHttpHandler } from "./mcp-http-handler.js";
26
+ import { autoConfigureDetectedMcpClients } from "./mcp-client-setup.js";
27
+ import { readOpenClawGatewayPort, readOpenClawSettingsSnapshot } from "./openclaw-settings.js";
25
28
  import { posthogCapture } from "./telemetry/posthog.js";
26
29
  export { OrgXClient } from "./api.js";
27
30
  const DEFAULT_BASE_URL = "https://www.useorgx.com";
@@ -364,14 +367,14 @@ export default function register(api) {
364
367
  const defaultReportingCorrelationId = pickNonEmptyString(process.env.ORGX_CORRELATION_ID) ??
365
368
  `openclaw-${config.installationId}`;
366
369
  function resolveReportingContext(input) {
367
- const initiativeId = pickNonEmptyString(input.initiative_id, process.env.ORGX_INITIATIVE_ID);
370
+ const initiativeId = pickNonEmptyString(input.initiative_id, input.initiativeId, process.env.ORGX_INITIATIVE_ID);
368
371
  if (!initiativeId || !isUuid(initiativeId)) {
369
372
  return {
370
373
  ok: false,
371
374
  error: "initiative_id is required (set ORGX_INITIATIVE_ID or pass initiative_id).",
372
375
  };
373
376
  }
374
- const sourceCandidate = pickNonEmptyString(input.source_client, process.env.ORGX_SOURCE_CLIENT, "openclaw");
377
+ const sourceCandidate = pickNonEmptyString(input.source_client, input.sourceClient, process.env.ORGX_SOURCE_CLIENT, "openclaw");
375
378
  const sourceClient = sourceCandidate === "codex" ||
376
379
  sourceCandidate === "claude-code" ||
377
380
  sourceCandidate === "api" ||
@@ -382,7 +385,10 @@ export default function register(api) {
382
385
  const runId = isUuid(runIdCandidate) ? runIdCandidate : undefined;
383
386
  const correlationId = runId
384
387
  ? undefined
385
- : pickNonEmptyString(input.correlation_id, defaultReportingCorrelationId, `openclaw-${Date.now()}`);
388
+ : pickNonEmptyString(input.correlation_id, input.correlationId,
389
+ // Legacy: some buffered payloads only stored a local `runId` which is
390
+ // better treated as a correlation key than a server-backed run_id.
391
+ input.runId, defaultReportingCorrelationId, `openclaw-${Date.now()}`);
386
392
  return {
387
393
  ok: true,
388
394
  value: {
@@ -622,6 +628,23 @@ export default function register(api) {
622
628
  installationId: config.installationId,
623
629
  workspaceName: input.workspaceName ?? onboardingState.workspaceName,
624
630
  });
631
+ if (input.source === "browser_pairing" &&
632
+ process.env.ORGX_DISABLE_MCP_CLIENT_AUTOCONFIG !== "1") {
633
+ try {
634
+ const snapshot = readOpenClawSettingsSnapshot();
635
+ const port = readOpenClawGatewayPort(snapshot.raw);
636
+ const localMcpUrl = `http://127.0.0.1:${port}/orgx/mcp`;
637
+ void autoConfigureDetectedMcpClients({
638
+ localMcpUrl,
639
+ logger: api.log ?? {},
640
+ }).catch(() => {
641
+ // best effort
642
+ });
643
+ }
644
+ catch {
645
+ // best effort
646
+ }
647
+ }
625
648
  }
626
649
  // ---------------------------------------------------------------------------
627
650
  // 1. Background Sync Service
@@ -629,7 +652,6 @@ export default function register(api) {
629
652
  let syncTimer = null;
630
653
  let syncInFlight = null;
631
654
  let syncServiceRunning = false;
632
- const outboxQueues = ["progress", "decisions", "artifacts"];
633
655
  let outboxReplayState = {
634
656
  status: "idle",
635
657
  lastReplayAttemptAt: null,
@@ -809,7 +831,11 @@ export default function register(api) {
809
831
  throw new Error(context.error);
810
832
  }
811
833
  const rawPhase = pickStringField(payload, "phase") ?? "implementing";
812
- const progressPct = typeof payload.progress_pct === "number" ? payload.progress_pct : undefined;
834
+ const progressPct = typeof payload.progress_pct === "number"
835
+ ? payload.progress_pct
836
+ : typeof payload.progressPct === "number"
837
+ ? payload.progressPct
838
+ : undefined;
813
839
  const phase = rawPhase === "intent" ||
814
840
  rawPhase === "execution" ||
815
841
  rawPhase === "blocked" ||
@@ -818,7 +844,11 @@ export default function register(api) {
818
844
  rawPhase === "completed"
819
845
  ? rawPhase
820
846
  : toReportingPhase(rawPhase, progressPct);
821
- await client.emitActivity({
847
+ const metaRaw = payload.metadata;
848
+ const meta = metaRaw && typeof metaRaw === "object" && !Array.isArray(metaRaw)
849
+ ? metaRaw
850
+ : {};
851
+ const emitPayload = {
822
852
  initiative_id: context.value.initiativeId,
823
853
  run_id: context.value.runId,
824
854
  correlation_id: context.value.correlationId,
@@ -827,12 +857,44 @@ export default function register(api) {
827
857
  phase,
828
858
  progress_pct: progressPct,
829
859
  level: pickStringField(payload, "level"),
830
- next_step: pickStringField(payload, "next_step") ?? undefined,
860
+ next_step: pickStringField(payload, "next_step") ??
861
+ pickStringField(payload, "nextStep") ??
862
+ undefined,
831
863
  metadata: {
864
+ ...meta,
832
865
  source: "orgx_openclaw_outbox_replay",
833
866
  outbox_event_id: event.id,
834
867
  },
835
- });
868
+ };
869
+ try {
870
+ await client.emitActivity(emitPayload);
871
+ }
872
+ catch (err) {
873
+ // Some locally-buffered events carry a UUID that *looks* like an OrgX run_id
874
+ // but was only ever used as a local correlation/grouping key. If OrgX
875
+ // doesn't recognize it, retry by treating it as correlation_id so OrgX can
876
+ // create/attach a run deterministically.
877
+ const msg = toErrorMessage(err);
878
+ if (emitPayload.run_id &&
879
+ /^404\\b/.test(msg) &&
880
+ /\\brun\\b/i.test(msg) &&
881
+ /not found/i.test(msg)) {
882
+ await client.emitActivity({
883
+ ...emitPayload,
884
+ run_id: undefined,
885
+ // Avoid passing a bare UUID as correlation_id since some server
886
+ // paths may interpret UUIDs as run_id lookups.
887
+ correlation_id: `openclaw:${emitPayload.run_id}`,
888
+ metadata: {
889
+ ...emitPayload.metadata,
890
+ replay_run_id_as_correlation: true,
891
+ },
892
+ });
893
+ }
894
+ else {
895
+ throw err;
896
+ }
897
+ }
836
898
  return;
837
899
  }
838
900
  if (event.type === "decision") {
@@ -918,7 +980,14 @@ export default function register(api) {
918
980
  };
919
981
  let hadReplayFailure = false;
920
982
  let lastReplayError = null;
921
- for (const queue of outboxQueues) {
983
+ // Outbox files are keyed by *session id* (e.g. initiative/run correlation),
984
+ // not by event type.
985
+ const outboxSummary = await readOutboxSummary();
986
+ const queues = Object.entries(outboxSummary.pendingByQueue)
987
+ .filter(([, count]) => typeof count === "number" && count > 0)
988
+ .map(([queueId]) => queueId)
989
+ .sort();
990
+ for (const queue of queues) {
922
991
  const pending = await readOutbox(queue);
923
992
  if (pending.length === 0) {
924
993
  continue;
@@ -1363,8 +1432,13 @@ export default function register(api) {
1363
1432
  // ---------------------------------------------------------------------------
1364
1433
  // 2. MCP Tools (Model Context Protocol compatible)
1365
1434
  // ---------------------------------------------------------------------------
1435
+ const mcpToolRegistry = new Map();
1436
+ const registerMcpTool = (tool, options) => {
1437
+ mcpToolRegistry.set(tool.name, tool);
1438
+ api.registerTool(tool, options);
1439
+ };
1366
1440
  // --- orgx_status ---
1367
- api.registerTool({
1441
+ registerMcpTool({
1368
1442
  name: "orgx_status",
1369
1443
  description: "Get current OrgX org status: active initiatives, agent states, pending decisions, active tasks.",
1370
1444
  parameters: {
@@ -1384,7 +1458,7 @@ export default function register(api) {
1384
1458
  },
1385
1459
  }, { optional: true });
1386
1460
  // --- orgx_sync ---
1387
- api.registerTool({
1461
+ registerMcpTool({
1388
1462
  name: "orgx_sync",
1389
1463
  description: "Push/pull memory sync with OrgX. Send local memory/daily log; receive initiatives, tasks, decisions, model routing policy.",
1390
1464
  parameters: {
@@ -1414,7 +1488,7 @@ export default function register(api) {
1414
1488
  },
1415
1489
  }, { optional: true });
1416
1490
  // --- orgx_delegation_preflight ---
1417
- api.registerTool({
1491
+ registerMcpTool({
1418
1492
  name: "orgx_delegation_preflight",
1419
1493
  description: "Run delegation preflight to score scope quality, estimate ETA/cost, and suggest a split before autonomous execution.",
1420
1494
  parameters: {
@@ -1465,7 +1539,7 @@ export default function register(api) {
1465
1539
  },
1466
1540
  }, { optional: true });
1467
1541
  // --- orgx_run_action ---
1468
- api.registerTool({
1542
+ registerMcpTool({
1469
1543
  name: "orgx_run_action",
1470
1544
  description: "Apply a control action to a run: pause, resume, cancel, or rollback (rollback requires checkpointId).",
1471
1545
  parameters: {
@@ -1509,7 +1583,7 @@ export default function register(api) {
1509
1583
  },
1510
1584
  }, { optional: true });
1511
1585
  // --- orgx_checkpoints_list ---
1512
- api.registerTool({
1586
+ registerMcpTool({
1513
1587
  name: "orgx_checkpoints_list",
1514
1588
  description: "List checkpoints for a run.",
1515
1589
  parameters: {
@@ -1534,7 +1608,7 @@ export default function register(api) {
1534
1608
  },
1535
1609
  }, { optional: true });
1536
1610
  // --- orgx_checkpoint_restore ---
1537
- api.registerTool({
1611
+ registerMcpTool({
1538
1612
  name: "orgx_checkpoint_restore",
1539
1613
  description: "Restore a run to a specific checkpoint.",
1540
1614
  parameters: {
@@ -1573,7 +1647,7 @@ export default function register(api) {
1573
1647
  },
1574
1648
  }, { optional: true });
1575
1649
  // --- orgx_spawn_check ---
1576
- api.registerTool({
1650
+ registerMcpTool({
1577
1651
  name: "orgx_spawn_check",
1578
1652
  description: "Check quality gate + get model routing before spawning a sub-agent. Returns allowed/denied, model tier, and check details.",
1579
1653
  parameters: {
@@ -1602,7 +1676,7 @@ export default function register(api) {
1602
1676
  },
1603
1677
  }, { optional: true });
1604
1678
  // --- orgx_quality_score ---
1605
- api.registerTool({
1679
+ registerMcpTool({
1606
1680
  name: "orgx_quality_score",
1607
1681
  description: "Record a quality score (1-5) for completed agent work. Used to gate future spawns and track performance.",
1608
1682
  parameters: {
@@ -1640,7 +1714,7 @@ export default function register(api) {
1640
1714
  },
1641
1715
  }, { optional: true });
1642
1716
  // --- orgx_create_entity ---
1643
- api.registerTool({
1717
+ registerMcpTool({
1644
1718
  name: "orgx_create_entity",
1645
1719
  description: "Create an OrgX entity (initiative, workstream, task, decision, milestone, etc.).",
1646
1720
  parameters: {
@@ -1725,7 +1799,7 @@ export default function register(api) {
1725
1799
  },
1726
1800
  }, { optional: true });
1727
1801
  // --- orgx_update_entity ---
1728
- api.registerTool({
1802
+ registerMcpTool({
1729
1803
  name: "orgx_update_entity",
1730
1804
  description: "Update an existing OrgX entity by type and ID.",
1731
1805
  parameters: {
@@ -1766,7 +1840,7 @@ export default function register(api) {
1766
1840
  },
1767
1841
  }, { optional: true });
1768
1842
  // --- orgx_list_entities ---
1769
- api.registerTool({
1843
+ registerMcpTool({
1770
1844
  name: "orgx_list_entities",
1771
1845
  description: "List OrgX entities of a given type with optional status filter.",
1772
1846
  parameters: {
@@ -1908,7 +1982,7 @@ export default function register(api) {
1908
1982
  }
1909
1983
  }
1910
1984
  // --- orgx_emit_activity ---
1911
- api.registerTool({
1985
+ registerMcpTool({
1912
1986
  name: "orgx_emit_activity",
1913
1987
  description: "Emit append-only OrgX activity telemetry (launch reporting contract primary write tool).",
1914
1988
  parameters: {
@@ -1968,7 +2042,7 @@ export default function register(api) {
1968
2042
  },
1969
2043
  }, { optional: true });
1970
2044
  // --- orgx_apply_changeset ---
1971
- api.registerTool({
2045
+ registerMcpTool({
1972
2046
  name: "orgx_apply_changeset",
1973
2047
  description: "Apply an idempotent transactional OrgX changeset (launch reporting contract primary mutation tool).",
1974
2048
  parameters: {
@@ -2011,7 +2085,7 @@ export default function register(api) {
2011
2085
  },
2012
2086
  }, { optional: true });
2013
2087
  // --- orgx_report_progress (alias -> orgx_emit_activity) ---
2014
- api.registerTool({
2088
+ registerMcpTool({
2015
2089
  name: "orgx_report_progress",
2016
2090
  description: "Alias for orgx_emit_activity. Report progress at key milestones so the team can track your work.",
2017
2091
  parameters: {
@@ -2074,7 +2148,7 @@ export default function register(api) {
2074
2148
  },
2075
2149
  }, { optional: true });
2076
2150
  // --- orgx_request_decision (alias -> orgx_apply_changeset decision.create) ---
2077
- api.registerTool({
2151
+ registerMcpTool({
2078
2152
  name: "orgx_request_decision",
2079
2153
  description: "Alias for orgx_apply_changeset with decision.create. Request a human decision before proceeding.",
2080
2154
  parameters: {
@@ -2159,7 +2233,7 @@ export default function register(api) {
2159
2233
  },
2160
2234
  }, { optional: true });
2161
2235
  // --- orgx_register_artifact ---
2162
- api.registerTool({
2236
+ registerMcpTool({
2163
2237
  name: "orgx_register_artifact",
2164
2238
  description: "Register a work output (PR, document, config change, report, etc.) with OrgX. Makes it visible in the dashboard.",
2165
2239
  parameters: {
@@ -2332,7 +2406,18 @@ export default function register(api) {
2332
2406
  }, {
2333
2407
  getHealth: async (input = {}) => buildHealthReport({ probeRemote: input.probeRemote === true }),
2334
2408
  });
2335
- api.registerHttpHandler(httpHandler);
2409
+ const mcpHttpHandler = createMcpHttpHandler({
2410
+ tools: mcpToolRegistry,
2411
+ logger: api.log ?? {},
2412
+ serverName: "@useorgx/openclaw-plugin",
2413
+ serverVersion: config.pluginVersion,
2414
+ });
2415
+ const compositeHttpHandler = async (req, res) => {
2416
+ if (await mcpHttpHandler(req, res))
2417
+ return true;
2418
+ return await httpHandler(req, res);
2419
+ };
2420
+ api.registerHttpHandler(compositeHttpHandler);
2336
2421
  api.log?.info?.("[orgx] Plugin registered", {
2337
2422
  baseUrl: config.baseUrl,
2338
2423
  hasApiKey: !!config.apiKey,
@@ -0,0 +1,30 @@
1
+ import type { Logger } from "./mcp-http-handler.js";
2
+ export declare function patchClaudeMcpConfig(input: {
3
+ current: Record<string, unknown>;
4
+ localMcpUrl: string;
5
+ }): {
6
+ updated: boolean;
7
+ next: Record<string, unknown>;
8
+ };
9
+ export declare function patchCursorMcpConfig(input: {
10
+ current: Record<string, unknown>;
11
+ localMcpUrl: string;
12
+ }): {
13
+ updated: boolean;
14
+ next: Record<string, unknown>;
15
+ };
16
+ export declare function patchCodexConfigToml(input: {
17
+ current: string;
18
+ localMcpUrl: string;
19
+ }): {
20
+ updated: boolean;
21
+ next: string;
22
+ };
23
+ export declare function autoConfigureDetectedMcpClients(input: {
24
+ localMcpUrl: string;
25
+ logger?: Logger;
26
+ homeDir?: string;
27
+ }): Promise<{
28
+ updatedPaths: string[];
29
+ skippedPaths: string[];
30
+ }>;
@@ -0,0 +1,215 @@
1
+ import { existsSync, readFileSync, statSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { randomUUID } from "node:crypto";
5
+ import { writeFileAtomicSync, writeJsonFileAtomicSync } from "./fs-utils.js";
6
+ function isRecord(value) {
7
+ return Boolean(value && typeof value === "object" && !Array.isArray(value));
8
+ }
9
+ function parseJsonObjectSafe(raw) {
10
+ try {
11
+ const parsed = JSON.parse(raw);
12
+ return isRecord(parsed) ? parsed : null;
13
+ }
14
+ catch {
15
+ return null;
16
+ }
17
+ }
18
+ function fileModeOrDefault(path, fallback) {
19
+ try {
20
+ const stat = statSync(path);
21
+ return stat.mode & 0o777;
22
+ }
23
+ catch {
24
+ return fallback;
25
+ }
26
+ }
27
+ function backupPath(path) {
28
+ return `${path}.bak.${Date.now()}-${randomUUID().slice(0, 8)}`;
29
+ }
30
+ function backupFileSync(path, mode) {
31
+ try {
32
+ const content = readFileSync(path);
33
+ const next = backupPath(path);
34
+ writeFileAtomicSync(next, content.toString("utf8"), { mode, encoding: "utf8" });
35
+ return next;
36
+ }
37
+ catch {
38
+ return null;
39
+ }
40
+ }
41
+ export function patchClaudeMcpConfig(input) {
42
+ const currentServers = isRecord(input.current.mcpServers) ? input.current.mcpServers : {};
43
+ const currentOrgx = isRecord(currentServers.orgx) ? currentServers.orgx : {};
44
+ const priorUrl = typeof currentOrgx.url === "string" ? currentOrgx.url : "";
45
+ const priorType = typeof currentOrgx.type === "string" ? currentOrgx.type : "";
46
+ const nextOrgx = {
47
+ ...currentOrgx,
48
+ type: "http",
49
+ url: input.localMcpUrl,
50
+ description: typeof currentOrgx.description === "string" && currentOrgx.description.trim().length > 0
51
+ ? currentOrgx.description
52
+ : "OrgX platform via local OpenClaw plugin (no OAuth)",
53
+ };
54
+ const nextServers = {
55
+ ...currentServers,
56
+ orgx: nextOrgx,
57
+ };
58
+ const next = {
59
+ ...input.current,
60
+ mcpServers: nextServers,
61
+ };
62
+ const updated = priorUrl !== input.localMcpUrl || priorType !== "http";
63
+ return { updated, next };
64
+ }
65
+ export function patchCursorMcpConfig(input) {
66
+ const currentServers = isRecord(input.current.mcpServers) ? input.current.mcpServers : {};
67
+ const key = "orgx-openclaw";
68
+ const existing = isRecord(currentServers[key]) ? currentServers[key] : {};
69
+ const priorUrl = typeof existing.url === "string" ? existing.url : "";
70
+ const nextEntry = {
71
+ ...existing,
72
+ url: input.localMcpUrl,
73
+ };
74
+ const nextServers = {
75
+ ...currentServers,
76
+ [key]: nextEntry,
77
+ };
78
+ const next = {
79
+ ...input.current,
80
+ mcpServers: nextServers,
81
+ };
82
+ const updated = priorUrl !== input.localMcpUrl;
83
+ return { updated, next };
84
+ }
85
+ export function patchCodexConfigToml(input) {
86
+ const lines = input.current.split(/\r?\n/);
87
+ const headerRegex = /^\[mcp_servers\.(?:orgx|"orgx")\]\s*$/;
88
+ let headerIndex = -1;
89
+ for (let i = 0; i < lines.length; i += 1) {
90
+ if (headerRegex.test(lines[i].trim())) {
91
+ headerIndex = i;
92
+ break;
93
+ }
94
+ }
95
+ const urlLine = `url = "${input.localMcpUrl}"`;
96
+ if (headerIndex === -1) {
97
+ const suffix = [
98
+ "",
99
+ "[mcp_servers.orgx]",
100
+ urlLine,
101
+ "",
102
+ ].join("\n");
103
+ const normalized = input.current.endsWith("\n") ? input.current : `${input.current}\n`;
104
+ return { updated: true, next: `${normalized}${suffix}` };
105
+ }
106
+ let sectionEnd = lines.length;
107
+ for (let i = headerIndex + 1; i < lines.length; i += 1) {
108
+ if (lines[i].trim().startsWith("[")) {
109
+ sectionEnd = i;
110
+ break;
111
+ }
112
+ }
113
+ let updated = false;
114
+ let urlIndex = -1;
115
+ for (let i = headerIndex + 1; i < sectionEnd; i += 1) {
116
+ if (/^\s*url\s*=/.test(lines[i])) {
117
+ urlIndex = i;
118
+ break;
119
+ }
120
+ }
121
+ if (urlIndex >= 0) {
122
+ if (lines[urlIndex].trim() !== urlLine) {
123
+ lines[urlIndex] = urlLine;
124
+ updated = true;
125
+ }
126
+ }
127
+ else {
128
+ lines.splice(headerIndex + 1, 0, urlLine);
129
+ updated = true;
130
+ }
131
+ return { updated, next: `${lines.join("\n")}\n` };
132
+ }
133
+ export async function autoConfigureDetectedMcpClients(input) {
134
+ const logger = input.logger ?? {};
135
+ const home = input.homeDir ?? homedir();
136
+ const updatedPaths = [];
137
+ const skippedPaths = [];
138
+ const targets = [
139
+ { kind: "claude", path: join(home, ".claude", "mcp.json") },
140
+ { kind: "cursor", path: join(home, ".cursor", "mcp.json") },
141
+ { kind: "codex", path: join(home, ".codex", "config.toml") },
142
+ ];
143
+ for (const target of targets) {
144
+ if (!existsSync(target.path)) {
145
+ skippedPaths.push(target.path);
146
+ continue;
147
+ }
148
+ const mode = fileModeOrDefault(target.path, 0o600);
149
+ try {
150
+ if (target.kind === "codex") {
151
+ const current = readFileSync(target.path, "utf8");
152
+ const patched = patchCodexConfigToml({ current, localMcpUrl: input.localMcpUrl });
153
+ if (!patched.updated) {
154
+ skippedPaths.push(target.path);
155
+ continue;
156
+ }
157
+ const backup = backupFileSync(target.path, mode);
158
+ if (!backup) {
159
+ logger.warn?.("[orgx] MCP client autoconfig: backup failed; skipping", {
160
+ path: target.path,
161
+ kind: target.kind,
162
+ });
163
+ skippedPaths.push(target.path);
164
+ continue;
165
+ }
166
+ writeFileAtomicSync(target.path, patched.next, { mode, encoding: "utf8" });
167
+ updatedPaths.push(target.path);
168
+ continue;
169
+ }
170
+ const currentText = readFileSync(target.path, "utf8");
171
+ const current = parseJsonObjectSafe(currentText);
172
+ if (!current) {
173
+ logger.warn?.("[orgx] MCP client autoconfig: invalid JSON; skipping", {
174
+ path: target.path,
175
+ kind: target.kind,
176
+ });
177
+ skippedPaths.push(target.path);
178
+ continue;
179
+ }
180
+ const patched = target.kind === "claude"
181
+ ? patchClaudeMcpConfig({ current, localMcpUrl: input.localMcpUrl })
182
+ : patchCursorMcpConfig({ current, localMcpUrl: input.localMcpUrl });
183
+ if (!patched.updated) {
184
+ skippedPaths.push(target.path);
185
+ continue;
186
+ }
187
+ const backup = backupFileSync(target.path, mode);
188
+ if (!backup) {
189
+ logger.warn?.("[orgx] MCP client autoconfig: backup failed; skipping", {
190
+ path: target.path,
191
+ kind: target.kind,
192
+ });
193
+ skippedPaths.push(target.path);
194
+ continue;
195
+ }
196
+ writeJsonFileAtomicSync(target.path, patched.next, mode);
197
+ updatedPaths.push(target.path);
198
+ }
199
+ catch (err) {
200
+ logger.warn?.("[orgx] MCP client autoconfig failed; leaving backup in place", {
201
+ path: target.path,
202
+ kind: target.kind,
203
+ error: err instanceof Error ? err.message : String(err),
204
+ });
205
+ skippedPaths.push(target.path);
206
+ }
207
+ }
208
+ if (updatedPaths.length > 0) {
209
+ logger.info?.("[orgx] MCP client autoconfig applied", {
210
+ localMcpUrl: input.localMcpUrl,
211
+ updated: updatedPaths,
212
+ });
213
+ }
214
+ return { updatedPaths, skippedPaths };
215
+ }
@@ -0,0 +1,38 @@
1
+ export type Logger = {
2
+ info?: (message: string, meta?: Record<string, unknown>) => void;
3
+ warn?: (message: string, meta?: Record<string, unknown>) => void;
4
+ debug?: (message: string, meta?: Record<string, unknown>) => void;
5
+ };
6
+ export interface PluginRequest {
7
+ method?: string;
8
+ url?: string;
9
+ headers: Record<string, string | string[] | undefined>;
10
+ body?: unknown;
11
+ on?: (event: string, listener: (...args: unknown[]) => void) => void;
12
+ once?: (event: string, listener: (...args: unknown[]) => void) => void;
13
+ }
14
+ export interface PluginResponse {
15
+ writeHead(status: number, headers?: Record<string, string>): void;
16
+ end(body?: string | Buffer): void;
17
+ write?(chunk: string | Buffer): boolean | void;
18
+ writableEnded?: boolean;
19
+ }
20
+ export type ToolResult = {
21
+ content: Array<{
22
+ type: "text";
23
+ text: string;
24
+ }>;
25
+ isError?: boolean;
26
+ };
27
+ export type RegisteredTool = {
28
+ name: string;
29
+ description: string;
30
+ parameters: Record<string, unknown>;
31
+ execute: (callId: string, params?: unknown) => Promise<ToolResult>;
32
+ };
33
+ export declare function createMcpHttpHandler(input: {
34
+ tools: Map<string, RegisteredTool>;
35
+ logger?: Logger;
36
+ serverName: string;
37
+ serverVersion: string;
38
+ }): (req: PluginRequest, res: PluginResponse) => Promise<boolean>;