@minhspark/codex-mcp-bridge 1.19.6 → 1.20.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.
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "codex-mcp-bridge",
3
+ "version": "1.20.0",
4
+ "description": "Connect ChatGPT to your own Codex Desktop projects and conversations.",
5
+ "author": {
6
+ "name": "Bui Dang Minh"
7
+ },
8
+ "homepage": "https://github.com/buidangminh23/codex-mcp-bridge",
9
+ "license": "MIT",
10
+ "skills": "./skills/",
11
+ "mcpServers": "./.mcp.json",
12
+ "interface": {
13
+ "displayName": "Codex MCP Bridge",
14
+ "shortDescription": "Connect to your Codex Desktop",
15
+ "longDescription": "Connect ChatGPT to your own Codex Desktop computer. List saved projects, read conversations, start an authorized task, or continue unfinished work. Each account pairs its own connector; local project roots and account identities are verified before dispatch. Requires Node.js 22+, Windows or macOS, signed-in Codex Desktop and Claude Desktop, the native relay, and a running outbound connector. Queued requests return an operation ID. Interrupted sends are never automatically repeated.",
16
+ "developerName": "Bui Dang Minh",
17
+ "composerIcon": "./assets/plugin-icon.svg",
18
+ "logo": "./assets/plugin-icon.svg",
19
+ "category": "Developer Tools",
20
+ "capabilities": ["List Codex projects", "Read Codex conversations", "Start Codex tasks", "Continue Codex tasks"],
21
+ "websiteURL": "https://codex-mcp-bridge.buidangminh23.chatgpt.site",
22
+ "supportURL": "https://github.com/buidangminh23/codex-mcp-bridge/issues",
23
+ "privacyPolicyURL": "https://github.com/buidangminh23/codex-mcp-bridge#hosted-data",
24
+ "termsOfServiceURL": "https://github.com/buidangminh23/codex-mcp-bridge/blob/main/LICENSE"
25
+ }
26
+ }
package/.mcp.json ADDED
@@ -0,0 +1,7 @@
1
+ {
2
+ "mcpServers": {
3
+ "codex-mcp-bridge": {
4
+ "url": "https://codex-mcp-bridge.buidangminh23.chatgpt.site/mcp"
5
+ }
6
+ }
7
+ }
package/CHANGELOG.md CHANGED
@@ -4,6 +4,22 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and [SemVer](ht
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [1.20.0] - 2026-10-03
8
+
9
+ ### Added
10
+
11
+ - Codex MCP Bridge plugin package and a public Sites MCP server. Each account pairs its own computer; users can access only their paired connector and its allowed local projects. Public ChatGPT directory publication remains subject to developer verification and review.
12
+ - Outbound HTTPS Desktop connector with account binding, private credentials, persistent operation receipts, and live pairing checks before sending work. Duplicate operation IDs cannot send the same prompt again; interrupted sends remain uncertain until inspected.
13
+ - Hosted Worker source, D1 migrations, pairing UI, plugin compatibility manifest, workflow skill, and Windows/macOS setup instructions.
14
+
15
+ ## [1.19.7] - 2026-10-02
16
+
17
+ ### Fixed
18
+
19
+ - Recover dormant Claude Desktop Code tasks by reopening their exact saved task ID before sending. Keep account, sender, project and recipient checks active throughout recovery; never substitute a CLI or another task.
20
+ - On Windows, submit authorized new Desktop conversations after verifying the exact prompt and explicitly selecting the full native project path. A deep-link folder hint can otherwise create a scratch "No folder" session. Authorized workspace trust handles only the exact native project dialog. Preserve uncertain submission receipts without duplicate Send and track native helper changes during runtime reload.
21
+ - Recover the Windows Claude URI launcher when an application update leaves the registered executable missing.
22
+
7
23
  ## [1.19.6] - 2026-10-02
8
24
 
9
25
  ### Fixed
package/README.md CHANGED
@@ -20,12 +20,54 @@ Starring the repository or downloading/installing a package does not subscribe y
20
20
 
21
21
  **[Open live dashboard — refreshes every 30 seconds](https://buidangminh23.github.io/codex-mcp-bridge/)**
22
22
 
23
+ The image below is an hourly snapshot. Each metric shows its source time and reporting window; delayed sources are marked stale. The live dashboard refreshes independently.
24
+
23
25
  [![Repository usage dashboard](https://raw.githubusercontent.com/buidangminh23/codex-mcp-bridge/analytics/dashboard.svg)](https://github.com/buidangminh23/codex-mcp-bridge/tree/analytics)
24
26
 
25
27
  [Full statistics and daily history](https://github.com/buidangminh23/codex-mcp-bridge/tree/analytics) · [Public aggregate JSON](https://raw.githubusercontent.com/buidangminh23/codex-mcp-bridge/analytics/data.json) · [How these metrics work](#repository-analytics)
26
28
 
27
29
  ## Installation
28
30
 
31
+ ### ChatGPT plugin
32
+
33
+ [Open Codex Bridge](https://codex-mcp-bridge.buidangminh23.chatgpt.site) to connect ChatGPT to **your own computer**. Each ChatGPT account pairs its own connector and can access only the local project directories selected during setup. Native Desktop mode supports Windows and macOS. Keep Codex Desktop and the connector running.
34
+
35
+ Install Node.js 22+, sign in to Codex Desktop and Claude Desktop, and save the intended project in Codex Desktop. The connector pins both local account identities and stops if either account changes. Install the native relay using the platform instructions below, then pair:
36
+
37
+ Windows PowerShell:
38
+
39
+ ```powershell
40
+ npm.cmd install -g @minhspark/codex-mcp-bridge@latest
41
+ codex-native-relay-install.cmd --desktop-tasks
42
+ codex-sites-connector.cmd --pair --site https://codex-mcp-bridge.buidangminh23.chatgpt.site --roots "C:\Projects\YourProject"
43
+ ```
44
+
45
+ macOS Terminal:
46
+
47
+ ```bash
48
+ npm install -g @minhspark/codex-mcp-bridge@latest
49
+ codex-native-relay-install --desktop-tasks
50
+ codex-sites-connector --pair --site https://codex-mcp-bridge.buidangminh23.chatgpt.site --roots "$HOME/YourProject"
51
+ ```
52
+
53
+ Open the pairing URL printed in the terminal, sign in with the ChatGPT account that will use the plugin, and choose **Connect this computer**. The Site owner can install its provisioned plugin from **Plugins → Personal → Created by you**. To restart an already paired connector, run `codex-sites-connector` (`codex-sites-connector.cmd` on Windows). To replace the paired computer, use **Disconnect existing computer** on a fresh pairing page first.
54
+
55
+ The public MCP endpoint is `https://codex-mcp-bridge.buidangminh23.chatgpt.site/mcp`; it uses Sign in with ChatGPT. A plugin compatibility ZIP is attached to the [1.20.0 release](https://github.com/buidangminh23/codex-mcp-bridge/releases/tag/v1.20.0) for manual import in clients that support plugin packages. ChatGPT personal accounts cannot share their Sites-provisioned plugin directly by invitation or share link. A public directory listing requires verified developer identity and OpenAI review; this release does not claim directory approval. See [OpenAI's Sites plugin access rules](https://help.openai.com/en/articles/20001547-hosting-a-plugin-with-chatgpt-sites).
56
+
57
+ Use a semicolon between multiple Windows roots or a colon on macOS. The relay's executor must belong to an allowed project. Credentials and operation receipts stay in the current OS user's private `~/.codex/sites-bridge` directory. The connector uses outbound HTTPS; no public port or tunnel is required.
58
+
59
+ The plugin can list projects, read conversations, create an authorized task, and continue an existing task. Requests return an operation ID; read its result before sending more work. An uncertain send is never automatically repeated. Disconnecting blocks future dispatch and cancels queued work; tasks already sent to Desktop keep running. Linux/WSL users can use the existing CLI bridge below; the hosted connector requires native Codex Desktop.
60
+
61
+ The hosted Worker source and database migrations are in `sites/codex-bridge`. The portable `plugin.json`, `mcp.json`, compatibility manifest, and workflow skill are included in the npm package and this repository. The plugin ZIP has its portable manifest directly at the archive root.
62
+
63
+ #### Hosted data
64
+
65
+ The hosted service is operated by Bui Dang Minh and runs on OpenAI Sites. Sign in with ChatGPT supplies a Site-scoped user identifier; the Worker uses it to bind a connector and isolate each account's operation records. The Worker does not use profile names or email addresses. OpenAI handles sign-in and hosting independently.
66
+
67
+ The database stores the account identifier, a connector identifier and token hash, connection timestamps, requested tool arguments (including task prompts), and the returned Desktop results. These records support polling and duplicate-send prevention. The service operator can access this database. Data is sent only to the paired computer and returned to the requesting account through the authenticated service. A local connector stores its credentials, account fingerprints and operation receipts in the OS user's private configuration directory.
68
+
69
+ Requests expire for dispatch after two minutes. Expiry is not data deletion: operation records remain stored to preserve dispatch history. Disconnecting revokes the device token and cancels undispatched work; it does not erase operation history or stop tasks already running in Desktop. For deletion or privacy requests, contact the operator through the repository's support link, without posting private prompts, account credentials or conversation content in a public issue. No advertising or analytics collection is implemented by this hosted Worker.
70
+
29
71
  ### GitHub Packages
30
72
 
31
73
  The repository-linked copy is [@buidangminh23/codex-mcp-bridge](https://github.com/buidangminh23/codex-mcp-bridge/packages)
@@ -336,7 +378,7 @@ For unresolved failures, [open an issue](https://github.com/buidangminh23/codex-
336
378
 
337
379
  ## Repository analytics
338
380
 
339
- The live dashboard polls the aggregate API every 30 seconds. Installation counts reflect reports received by the server; this is not a count of currently online processes. Public GitHub/npm sources are refreshed with a short cache, but their own statistics may be delayed. GitHub Actions refreshes and archives statistics hourly through the `Repository analytics` workflow, including private GitHub traffic; scheduled runs may be delayed by GitHub. That workflow reads GitHub traffic with the `ANALYTICS_TOKEN` repository secret, a token with push access to this repository (fine-grained: Administration read); the default workflow token cannot read GitHub traffic, so without it the views/clones sources are retained from the last successful run and the run is reported as incomplete while the Pages deployment still succeeds. The `analytics` branch is written with the workflow's own token, so its commits come from `github-actions[bot]` and `ANALYTICS_TOKEN` needs no write access. The README image is a snapshot and may be cached by GitHub; open the live dashboard for automatic updates. Source timestamps show freshness; missing data is unavailable, not zero. Only aggregate figures are published. Installation IDs stay in the private database.
381
+ The live dashboard polls the aggregate API every 30 seconds. Installation counts reflect reports received by the server; this is not a count of currently online processes. Public GitHub/npm sources are refreshed with a short cache, but their own statistics may be delayed. GitHub Actions refreshes and archives statistics hourly through the `Repository analytics` workflow, including private GitHub traffic; scheduled runs may be delayed by GitHub. Data collection and publication to the `analytics` branch run independently of the Pages deployment environment, and a newer scheduled run replaces a stuck older run. That workflow reads GitHub traffic with the `ANALYTICS_TOKEN` repository secret, a token with push access to this repository (fine-grained: Administration read); the default workflow token cannot read GitHub traffic, so without it the views/clones sources are retained from the last successful run and the run is reported as incomplete. The `analytics` branch is written with the workflow's own token, so its commits come from `github-actions[bot]` and `ANALYTICS_TOKEN` needs no write access. The README image is a snapshot and may be cached by GitHub; open the live dashboard for automatic updates. Each source keeps its own collection time and reporting window. Sources older than three hours are marked stale even when polling succeeds. Missing data is unavailable, not zero; an empty usage breakdown means no opted-in reports in that period. Only aggregate figures are published. Installation IDs stay in the private database.
340
382
 
341
383
  Repository owners can view [GitHub traffic](https://github.com/buidangminh23/codex-mcp-bridge/graphs/traffic) for recent views and clones. Downloads and clones include updates, reinstalls, and automation; they do not measure active users. GitHub traffic only covers the recent 14-day window, so collect it regularly to keep a longer history.
342
384
 
@@ -0,0 +1,5 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="256" height="256" viewBox="0 0 256 256">
2
+ <rect width="256" height="256" rx="56" fill="#16202b"/>
3
+ <path d="M92 64 32 128 92 192M164 64 224 128 164 192" fill="none" stroke="#6ee7b7" stroke-width="20" stroke-linecap="round" stroke-linejoin="round"/>
4
+ <path d="M143 80 113 176" fill="none" stroke="#e7edf4" stroke-width="14" stroke-linecap="round"/>
5
+ </svg>
package/mcp.json ADDED
@@ -0,0 +1,9 @@
1
+ {
2
+ "$schema": "https://agent-plugins.org/schemas/1.0.0/mcp.schema.json",
3
+ "mcpServers": {
4
+ "codex-mcp-bridge": {
5
+ "type": "streamable-http",
6
+ "url": "https://codex-mcp-bridge.buidangminh23.chatgpt.site/mcp"
7
+ }
8
+ }
9
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@minhspark/codex-mcp-bridge",
3
- "version": "1.19.6",
3
+ "version": "1.20.0",
4
4
  "description": "Two-way MCP bridge between Claude and Codex: prompts into a live Codex thread, messages into a running Claude Code session.",
5
5
  "keywords": [
6
6
  "mcp",
@@ -29,11 +29,18 @@
29
29
  "codex-native-relay": "src/native-relay-launcher.mjs",
30
30
  "codex-native-relay-install": "scripts/install-native-relay.mjs",
31
31
  "codex-npm-footer-install": "scripts/install-npm-footer.mjs",
32
- "codex-full-access": "scripts/repair-codex-access.mjs"
32
+ "codex-full-access": "scripts/repair-codex-access.mjs",
33
+ "codex-sites-connector": "src/sites-connector.mjs"
33
34
  },
34
35
  "files": [
35
36
  "src",
36
37
  "scripts",
38
+ "skills",
39
+ "assets",
40
+ "plugin.json",
41
+ "mcp.json",
42
+ ".codex-plugin",
43
+ ".mcp.json",
37
44
  "README.md",
38
45
  "LICENSE",
39
46
  "CHANGELOG.md"
@@ -60,7 +67,7 @@
60
67
  "install:footer": "node scripts/install-npm-footer.mjs",
61
68
  "uninstall:footer": "node scripts/install-npm-footer.mjs --remove",
62
69
  "uninstall:agent": "node scripts/install-launch-agent.mjs --uninstall",
63
- "version": "node scripts/sync-version.mjs && git add src/index.mjs src/claude-bridge.mjs src/native-relay-companion.mjs scripts/install-native-relay.mjs"
70
+ "version": "node scripts/sync-version.mjs && git add src/index.mjs src/claude-bridge.mjs src/native-relay-companion.mjs scripts/install-native-relay.mjs .codex-plugin/plugin.json"
64
71
  },
65
72
  "engines": {
66
73
  "node": ">=22"
package/plugin.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
3
+ "name": "codex-mcp-bridge",
4
+ "version": "1.20.0",
5
+ "description": "Connect ChatGPT to your own Codex Desktop projects and conversations.",
6
+ "author": {
7
+ "name": "Bui Dang Minh",
8
+ "email": "buidangminh23@gmail.com",
9
+ "url": "https://github.com/buidangminh23"
10
+ },
11
+ "homepage": "https://github.com/buidangminh23/codex-mcp-bridge",
12
+ "repository": "https://github.com/buidangminh23/codex-mcp-bridge",
13
+ "license": "MIT",
14
+ "keywords": ["codex", "mcp", "developer-tools"],
15
+ "extensions": {
16
+ "com.openai": {
17
+ "interface": {
18
+ "displayName": "Codex MCP Bridge",
19
+ "shortDescription": "Connect to your Codex Desktop",
20
+ "longDescription": "Connect ChatGPT to your own Codex Desktop computer. Read saved projects and conversations, start an authorized task, or continue unfinished work. Requires Node.js 22+, Windows or macOS, signed-in Codex Desktop and Claude Desktop, the native relay, and a running outbound connector. Each account pairs its own computer and chooses allowed project roots. Interrupted sends are never automatically repeated.",
21
+ "developerName": "Bui Dang Minh",
22
+ "composerIcon": "./assets/plugin-icon.svg",
23
+ "logo": "./assets/plugin-icon.svg",
24
+ "category": "Developer Tools",
25
+ "capabilities": ["Read", "Write"],
26
+ "websiteURL": "https://codex-mcp-bridge.buidangminh23.chatgpt.site",
27
+ "supportURL": "https://github.com/buidangminh23/codex-mcp-bridge/issues",
28
+ "privacyPolicyURL": "https://github.com/buidangminh23/codex-mcp-bridge#hosted-data",
29
+ "termsOfServiceURL": "https://github.com/buidangminh23/codex-mcp-bridge/blob/main/LICENSE"
30
+ }
31
+ }
32
+ }
33
+ }
@@ -23,7 +23,7 @@ import { enableCodexFullAccess, fullAccessEnabled } from "../src/codex-full-acce
23
23
 
24
24
  exitForVersionRequest(import.meta.url);
25
25
 
26
- const VERSION = "1.19.6";
26
+ const VERSION = "1.20.0";
27
27
  const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
28
28
  const entry = path.join(root, "src", "mcp-supervisor.mjs");
29
29
  const entryArgs = ["native-relay-companion.mjs"];
@@ -9,6 +9,8 @@ const number = value => Number.isSafeInteger(value) && value >= 0 ? value : null
9
9
  const date = value => typeof value === 'string' && /^\d{4}-\d{2}-\d{2}(?:T[\d:.]+Z)?$/.test(value) && Number.isFinite(Date.parse(value)) ? value : null;
10
10
  const numericFields = (value, keys) => Object.fromEntries(keys.map(key => [key, number(value?.[key])]));
11
11
  const format = value => value == null ? 'Unavailable' : value.toLocaleString('en-US');
12
+ const displayTime = value => date(value) ? new Date(value).toISOString().slice(0, 16).replace('T', ' ') + ' UTC' : 'Unavailable';
13
+ const staleAfter = 3 * 60 * 60 * 1000;
12
14
 
13
15
  function sanitizeSnapshot(snapshot) {
14
16
  const output = { collectedAt: date(snapshot.collectedAt) };
@@ -44,17 +46,19 @@ export function renderPublicDashboard(data) {
44
46
  const releases = latest('releases')?.releases;
45
47
  const assets = releases?.flatMap(release => release.assets.map(asset => ({ ...asset, tag: release.tag }))) || [];
46
48
  const sum = rows => rows?.every(row => row.downloads != null) ? rows.reduce((total, row) => total + row.downloads, 0) : null;
47
- const window = (source, rows) => `${rows?.[0]?.timestamp?.slice(0, 10) || '?'} to ${rows?.at(-1)?.timestamp?.slice(0, 10) || '?'} UTC`;
49
+ const window = rows => rows?.length ? `${rows[0].timestamp?.slice(0, 10) || '?'} to ${rows.at(-1).timestamp?.slice(0, 10) || '?'} UTC` : 'Source window unavailable';
50
+ const sourceTime = source => latest(source)?.sourceCollectedAt?.[source];
51
+ const noReports = data.usage?.active.month === 0;
48
52
  const cards = [
49
- ['GitHub views', views?.count, window('views', views?.views)], ['Unique visitors', views?.uniques, 'Distinct within GitHub window'], ['GitHub clones', clones?.count, window('clones', clones?.clones)],
50
- ['Unique cloners', clones?.uniques, 'Distinct within GitHub window'], ['npm downloads', sum(npm?.downloads), npm ? `${npm.start} to ${npm.end} UTC` : 'Source unavailable'], ['Stars', repository?.stars, 'Current repository total'],
51
- ['Forks', repository?.forks, 'Current repository total'], ['Subscribers', repository?.subscribers, 'Current repository total'], ['Release downloads', releases ? sum(assets) : null, 'Cumulative asset downloads'],
53
+ ['GitHub views', views?.count, window(views?.views), sourceTime('views')], ['Unique visitors', views?.uniques, window(views?.views), sourceTime('views')], ['GitHub clones', clones?.count, window(clones?.clones), sourceTime('clones')],
54
+ ['Unique cloners', clones?.uniques, window(clones?.clones), sourceTime('clones')], ['npm downloads', sum(npm?.downloads), npm ? `${npm.start} to ${npm.end} UTC` : 'Source unavailable', sourceTime('npm')], ['Stars', repository?.stars, 'Repository total at source time', sourceTime('repository')],
55
+ ['Forks', repository?.forks, 'Repository total at source time', sourceTime('repository')], ['Subscribers', repository?.subscribers, 'Repository total at source time', sourceTime('repository')], ['Release downloads', releases ? sum(assets) : null, 'Cumulative asset downloads', sourceTime('releases')],
52
56
  ];
53
- if (data.usage) cards.push(['Active installations / day', data.usage.active.day, 'Consenting installations only'], ['Active installations / 7 days', data.usage.active.week, 'Distinct across the whole period'], ['Active installations / 30 days', data.usage.active.month, 'Distinct across the whole period']);
57
+ for (const [label, key] of [['Active installations / day', 'day'], ['Active installations / 7 days', 'week'], ['Active installations / 30 days', 'month']]) cards.push([label, data.usage?.active[key], noReports ? 'No opted-in reports in this period' : 'Consenting installations only', data.usage?.collectedAt]);
54
58
  const details = [
55
- ['Release assets', ...assets.map(asset => `${asset.tag} / ${asset.name}: ${format(asset.downloads)}`)],
56
- ['Operating systems / 30 days', ...(data.usage?.platforms || []).map(row => `${row.platform}: ${format(row.installations)}`)],
57
- ['Versions / 30 days', ...(data.usage?.versions || []).map(row => `${row.version}: ${format(row.installations)}`)],
59
+ ['Release assets', ...(assets.length ? assets.map(asset => `${asset.tag} / ${asset.name}: ${format(asset.downloads)}`) : [releases ? 'No release assets' : 'Source unavailable'])],
60
+ ['Operating systems / 30 days', ...(data.usage?.platforms.length ? data.usage.platforms.map(row => `${row.platform}: ${format(row.installations)}`) : [noReports ? 'No opted-in reports in this period' : 'Source unavailable'])],
61
+ ['Versions / 30 days', ...(data.usage?.versions.length ? data.usage.versions.map(row => `${row.version}: ${format(row.installations)}`) : [noReports ? 'No opted-in reports in this period' : 'Source unavailable'])],
58
62
  ['Source freshness (UTC)', ...['views', 'clones', 'npm', 'repository', 'releases'].map(source => `${source}: ${latest(source)?.sourceCollectedAt?.[source] || 'Unavailable'}`), ...(data.usage ? [`usage: ${data.usage.collectedAt || 'Unavailable'}`] : [])],
59
63
  ];
60
64
  const wrap = (value, maximum) => {
@@ -73,18 +77,21 @@ export function renderPublicDashboard(data) {
73
77
  const body = [];
74
78
  let y = 48;
75
79
  for (const line of wrap(data.repo, 42)) { body.push(text(line, 32, y, 28, '#f4f7fd')); y += 36; }
76
- for (const line of wrap(`Public aggregate analytics • Updated ${data.updatedAt || 'Unavailable'} (UTC)`, 65)) { body.push(text(line, 32, y)); y += 28; }
80
+ body.push(text('Public aggregate analytics · Hourly snapshot', 32, y)); y += 28;
81
+ body.push(text(`Generated: ${displayTime(data.updatedAt)}`, 32, y, 17)); y += 28;
77
82
  const cardTop = y + 12;
78
- cards.forEach(([label, value, detail], index) => {
83
+ cards.forEach(([label, value, detail, time], index) => {
79
84
  const x = 32 + index % 2 * 390;
80
- const top = cardTop + Math.floor(index / 2) * 176;
81
- body.push(`<rect x="${x}" y="${top}" width="366" height="160" rx="12" fill="#18253b"/>`);
82
- wrap(label, 28).forEach((line, row) => body.push(text(line, x + 16, top + 30 + row * 25, 22)));
85
+ const top = cardTop + Math.floor(index / 2) * 192;
86
+ body.push(`<rect x="${x}" y="${top}" width="366" height="176" rx="12" fill="#18253b"/>`);
87
+ wrap(label, 28).forEach((line, row) => body.push(text(line, x + 16, top + 30 + row * 25, 20)));
83
88
  body.push(text(format(value), x + 16, top + 103, 40, '#7ee5c0'));
84
- wrap(detail, 43).forEach((line, row) => body.push(text(line, x + 16, top + 132 + row * 18, 15)));
89
+ wrap(detail, 43).forEach((line, row) => body.push(text(line, x + 16, top + 130 + row * 16, 14)));
90
+ const stale = date(time) && Date.parse(data.updatedAt) - Date.parse(time) > staleAfter;
91
+ body.push(text(`${stale ? 'Stale · ' : 'Source: '}${displayTime(time)}`, x + 16, top + 161, 13, stale ? '#ffcb80' : '#acbcd0'));
85
92
  });
86
- y = cardTop + cardRows * 176 + 16;
87
- for (const caveat of ['Downloads and clones are not people or active users. CI, updates and reinstalls count.', 'Daily unique counts must not be added across dates. Missing values are unavailable, not zero.', data.usage ? 'Usage covers opted-in installations only; one person may use multiple installations.' : 'Active installation metrics unavailable; no aggregate usage report supplied.']) {
93
+ y = cardTop + cardRows * 192 + 16;
94
+ for (const caveat of ['Snapshot freshness is relative to generation time. Open the live dashboard for current age.', 'Downloads and clones are not people or active users. CI, updates and reinstalls count.', 'Daily unique counts must not be added across dates. Missing values are unavailable, not zero.', data.usage ? 'Usage covers opted-in installations only; OS and version data require telemetry consent.' : 'Active installation metrics unavailable; no aggregate usage report supplied.']) {
88
95
  for (const line of wrap(caveat, 70)) { body.push(text(line, 32, y)); y += 28; }
89
96
  y += 8;
90
97
  }
@@ -104,36 +111,77 @@ export function renderLiveDashboard(data) {
104
111
  const initial = { usage: data.usage, repository: latest('repository') ? { ...latest('repository').repository, collectedAt: latest('repository').sourceCollectedAt.repository } : undefined, npm: npm ? { start: npm.npm.start, end: npm.npm.end, downloads: npm.npm.downloads.every(row => row.downloads != null) ? npm.npm.downloads.reduce((sum, row) => sum + row.downloads, 0) : null, collectedAt: npm.sourceCollectedAt.npm } : undefined, traffic: { views: latest('views')?.views, clones: latest('clones')?.clones, collectedAt: latest('views')?.sourceCollectedAt.views, viewsCollectedAt: latest('views')?.sourceCollectedAt.views, clonesCollectedAt: latest('clones')?.sourceCollectedAt.clones } };
105
112
  initial.releases = latest('releases')?.releases;
106
113
  initial.releasesCollectedAt = latest('releases')?.sourceCollectedAt.releases;
114
+ for (const source of ['views', 'clones']) {
115
+ const rows = initial.traffic[source]?.[source];
116
+ if (rows?.length) initial.traffic[source] = { ...initial.traffic[source], start: rows[0].timestamp?.slice(0, 10), end: rows.at(-1).timestamp?.slice(0, 10) };
117
+ }
107
118
  const serialized = JSON.stringify(initial).replace(/</g, '\\u003c').replace(/>/g, '\\u003e').replace(/&/g, '\\u0026');
108
119
  return `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; connect-src https://vdbwkowdggtcihixowxi.supabase.co; base-uri 'none'"><title>Live repository analytics</title><style>
109
120
  *{box-sizing:border-box}body{margin:0;background:#0d1626;color:#f4f7fd;font:17px/1.6 system-ui,sans-serif}main{max-width:1100px;margin:auto;padding:32px 20px}h1{font-size:32px;line-height:1.2}.cards{display:grid;grid-template-columns:repeat(auto-fit,minmax(230px,1fr));gap:16px;margin:24px 0}article,.panel{background:#18253b;border-radius:12px;padding:20px}h2{font-size:19px;margin:0 0 8px}strong{font-size:38px;color:#7ee5c0;display:block}small,.muted{color:#acbcd0;display:block;font-size:14px}a{color:#9bcaff}.stale{color:#ffcb80}.panels{display:grid;grid-template-columns:repeat(auto-fit,minmax(250px,1fr));gap:16px}ul{padding-left:20px}#status{min-height:28px}footer{margin:24px 0}</style></head><body><main><h1>${escapeXml(data.repo)}</h1><p>Live aggregate dashboard</p><p id="status" role="status">Loading current statistics…</p><p class="muted">Refreshes every 30 seconds while visible. GitHub and npm source data can be delayed; source timestamps appear on every card. Active installations include consenting installations only.</p><div class="cards" id="cards"></div><div class="panels"><section class="panel"><h2>Operating systems / 30 days</h2><ul id="platforms"></ul></section><section class="panel"><h2>Versions / 30 days</h2><ul id="versions"></ul></section></div><footer>Downloads and clones are not people. Never add daily unique counts across dates. Missing metrics are unavailable, not zero.<p><a href="https://github.com/${data.repo}/tree/analytics">Full archived daily history and release assets</a> · <a href="data.json">Aggregate JSON</a></p></footer></main><script>
110
121
  let state = ${serialized};
122
+ let sourceErrors = new Set();
123
+ let lastRefresh = null;
124
+ let refreshFailed = false;
111
125
  const releasesPanel=document.createElement('section');releasesPanel.className='panel';const releasesTitle=document.createElement('h2');releasesTitle.textContent='Release assets';const releasesList=document.createElement('ul');releasesList.id='release-assets';releasesPanel.append(releasesTitle,releasesList);document.querySelector('.panels').append(releasesPanel);
112
126
  const format = value => Number.isSafeInteger(value) && value >= 0 ? value.toLocaleString('en-US') : 'Unavailable';
113
- const timestamp = value => typeof value === 'string' && Number.isFinite(Date.parse(value)) ? new Date(value).toISOString() : 'Unavailable';
127
+ const validTime = value => typeof value === 'string' && Number.isFinite(Date.parse(value));
128
+ const timestamp = value => validTime(value) ? new Date(value).toISOString().slice(0,16).replace('T',' ')+' UTC' : 'Unavailable';
129
+ const validCount = value => Number.isSafeInteger(value) && value >= 0;
130
+ const stale = time => !validTime(time) || Date.now()-Date.parse(time)>10800000;
131
+ const sourceWindow = metric => metric?.start && metric?.end ? metric.start+' to '+metric.end+' UTC' : 'Source window unavailable';
132
+ function statusText() {
133
+ const old = ['usage','repository','npm'].filter(source => stale(state[source]?.collectedAt) || sourceErrors.has(source));
134
+ for(const source of ['views','clones']) if(stale(state.traffic?.[source+'CollectedAt'] || state.traffic?.collectedAt) || sourceErrors.has(source)) old.push(source);
135
+ if(stale(state.releasesCollectedAt) || sourceErrors.has('releases')) old.push('releases');
136
+ const status=document.getElementById('status');
137
+ status.className=refreshFailed || old.length?'stale':'';
138
+ status.textContent=refreshFailed?'Refresh failed — showing previously available values. Retrying in 30 seconds.':(old.length?'Some sources stale or unavailable: '+old.join(', ')+' · ':'')+(lastRefresh?'Last API refresh: '+timestamp(lastRefresh):'Showing archived snapshot; requesting current statistics…');
139
+ }
114
140
  function render() {
115
141
  const u = state.usage || {}, r = state.repository || {}, n = state.npm || {}, t = state.traffic || {};
116
142
  const assets=Array.isArray(state.releases)?state.releases.flatMap(release=>(Array.isArray(release.assets)?release.assets:[]).map(asset=>({...asset,tag:release.tag}))):null;
117
143
  const releaseTotal=assets && assets.every(asset=>Number.isSafeInteger(asset.downloads)&&asset.downloads>=0)?assets.reduce((sum,asset)=>sum+asset.downloads,0):null;
118
- const rows = [['Reporting installations / day',u.active?.day,u.collectedAt],['Reporting installations / 7 days',u.active?.week,u.collectedAt],['Reporting installations / 30 days',u.active?.month,u.collectedAt],['GitHub views',t.views?.count,t.viewsCollectedAt || t.collectedAt],['Unique visitors',t.views?.uniques,t.viewsCollectedAt || t.collectedAt],['GitHub clones',t.clones?.count,t.clonesCollectedAt || t.collectedAt],['Unique cloners',t.clones?.uniques,t.clonesCollectedAt || t.collectedAt],['npm downloads',n.downloads,n.collectedAt,n.start && n.end ? n.start+' to '+n.end+' UTC' : 'Source window unavailable'],['Stars',r.stars,r.collectedAt],['Forks',r.forks,r.collectedAt],['Subscribers',r.subscribers,r.collectedAt],['Release downloads',releaseTotal,state.releasesCollectedAt,'Cumulative asset downloads']];
119
- document.getElementById('cards').replaceChildren(...rows.map(([label,value,time,detail]) => { const card=document.createElement('article'); const title=document.createElement('h2');title.textContent=label;const count=document.createElement('strong');count.textContent=format(value);const source=document.createElement('small');source.textContent='Source: '+timestamp(time);card.append(title,count,source);if(detail){const note=document.createElement('small');note.textContent=detail;card.append(note);}return card;}));
120
- for(const [id,key] of [['platforms','platform'],['versions','version']]){const list=Array.isArray(u[id])?u[id]:[];document.getElementById(id).replaceChildren(...(list.length?list:[{}]).map(row=>{const item=document.createElement('li');item.textContent=row[key]?String(row[key])+': '+format(row.installations):'Unavailable';return item;}));}
144
+ const noReports=u.active?.month===0;
145
+ const usageNote=noReports?'No opted-in reports in this period':'Consenting installations only';
146
+ const rows = [['Reporting installations / day',u.active?.day,u.collectedAt,usageNote,'usage'],['Reporting installations / 7 days',u.active?.week,u.collectedAt,usageNote,'usage'],['Reporting installations / 30 days',u.active?.month,u.collectedAt,usageNote,'usage'],['GitHub views',t.views?.count,t.viewsCollectedAt || t.collectedAt,sourceWindow(t.views),'views'],['Unique visitors',t.views?.uniques,t.viewsCollectedAt || t.collectedAt,sourceWindow(t.views),'views'],['GitHub clones',t.clones?.count,t.clonesCollectedAt || t.collectedAt,sourceWindow(t.clones),'clones'],['Unique cloners',t.clones?.uniques,t.clonesCollectedAt || t.collectedAt,sourceWindow(t.clones),'clones'],['npm downloads',n.downloads,n.collectedAt,sourceWindow(n),'npm'],['Stars',r.stars,r.collectedAt,null,'repository'],['Forks',r.forks,r.collectedAt,null,'repository'],['Subscribers',r.subscribers,r.collectedAt,null,'repository'],['Release downloads',releaseTotal,state.releasesCollectedAt,'Cumulative asset downloads','releases']];
147
+ document.getElementById('cards').replaceChildren(...rows.map(([label,value,time,detail,key]) => { const card=document.createElement('article'); const title=document.createElement('h2');title.textContent=label;const count=document.createElement('strong');count.textContent=format(value);const source=document.createElement('small');const old=stale(time)||sourceErrors.has(key)||refreshFailed;source.className=old?'stale':'';source.textContent='Source: '+timestamp(time)+(old?' · Stale or unavailable':'');card.append(title,count,source);if(detail){const note=document.createElement('small');note.textContent=detail;card.append(note);}return card;}));
148
+ for(const [id,key] of [['platforms','platform'],['versions','version']]){const list=Array.isArray(u[id])?u[id]:[];document.getElementById(id).replaceChildren(...(list.length?list:[{}]).map(row=>{const item=document.createElement('li');item.textContent=row[key]?String(row[key])+': '+format(row.installations):noReports?'No opted-in reports in this period. OS and version data require telemetry consent.':'Unavailable — no valid usage breakdown received.';return item;}));const source=document.createElement('li');source.className=stale(u.collectedAt)||sourceErrors.has('usage')||refreshFailed?'stale':'muted';source.textContent='Source: '+timestamp(u.collectedAt)+(source.className==='stale'?' · Stale or unavailable':'');document.getElementById(id).append(source);}
121
149
  releasesList.replaceChildren(...(assets?.length?assets:[null]).map(asset=>{const item=document.createElement('li');item.textContent=asset?String(asset.tag)+' / '+String(asset.name)+': '+format(asset.downloads):assets?'No release assets':'Unavailable';return item;}));
150
+ statusText();
151
+ }
152
+ function mergeResponse(next) {
153
+ const errors=new Set(Array.isArray(next.errors)?next.errors.map(row=>row.source):[]);
154
+ for(const [source,fields] of [['repository',['stars','forks','subscribers']],['npm',['downloads']]]) {
155
+ const value=next[source];
156
+ if(value && validTime(value.collectedAt) && fields.every(key=>validCount(value[key]))) state[source]=value;
157
+ else errors.add(source);
158
+ }
159
+ const u=next.usage;
160
+ if(u && validTime(u.collectedAt) && ['day','week','month'].every(key=>validCount(u.active?.[key])) && ['daily','platforms','versions'].every(key=>Array.isArray(u[key]) && u[key].every(row=>row && typeof row==='object' && validCount(row.installations)))) state.usage=u;
161
+ else errors.add('usage');
162
+ state.traffic=state.traffic || {};
163
+ for(const source of ['views','clones']) {
164
+ const metric=next.traffic?.[source];
165
+ const time=next.traffic?.[source+'CollectedAt'] || next.traffic?.collectedAt || next.sourceCollectedAt?.[source];
166
+ if(metric && validCount(metric.count) && validCount(metric.uniques) && validTime(time)) {state.traffic[source]=metric;state.traffic[source+'CollectedAt']=time;}
167
+ else errors.add(source);
168
+ }
169
+ const time=next.releasesCollectedAt || next.sourceCollectedAt?.releases;
170
+ if(Array.isArray(next.releases) && validTime(time) && next.releases.every(row=>typeof row.tag==='string' && Array.isArray(row.assets) && row.assets.every(asset=>typeof asset.name==='string' && validCount(asset.downloads)))) {state.releases=next.releases;state.releasesCollectedAt=time;}
171
+ else errors.add('releases');
172
+ sourceErrors=errors;
122
173
  }
123
174
  let pending = false;
124
175
  async function refresh() {
125
176
  if(pending || document.hidden) return;
126
177
  pending=true;
127
- const status=document.getElementById('status');
128
178
  try {
129
179
  const response=await fetch('https://vdbwkowdggtcihixowxi.supabase.co/functions/v1/bridge-stats',{headers:{apikey:'sb_publishable_2GIhGKL82wM8mN44L-Gzxw_Zgd-Vioh'},cache:'no-store',signal:AbortSignal.timeout(8000)});
130
180
  if(!response.ok) throw new Error('request failed');
131
181
  const next=await response.json();
132
- if(!next || typeof next!=='object' || !next.collectedAt) throw new Error('invalid response');
133
- for(const source of ['usage','repository','npm','traffic']){if(next[source] && typeof next[source]==='object') state[source]=next[source];}
134
- if(Array.isArray(next.releases)){state.releases=next.releases;state.releasesCollectedAt=next.releasesCollectedAt || next.sourceCollectedAt?.releases || null;}
135
- render();const partial=Array.isArray(next.errors)&&next.errors.length>0;status.className=partial?'stale':'';status.textContent=(partial?'Some sources stale · Last refresh: ':'Last refresh: ')+new Date().toISOString()+' · Source delays still apply';
136
- }catch{status.className='stale';status.textContent='Refresh failed — showing previously available values. Retrying in 30 seconds.';}
182
+ if(!next || typeof next!=='object' || !validTime(next.collectedAt)) throw new Error('invalid response');
183
+ mergeResponse(next);lastRefresh=new Date().toISOString();refreshFailed=false;render();
184
+ }catch{refreshFailed=true;render();}
137
185
  finally{pending=false;}
138
186
  }
139
187
  render();void refresh();setInterval(()=>{void refresh();},30000);document.addEventListener('visibilitychange',()=>{if(!document.hidden) void refresh();});
@@ -35,3 +35,10 @@ for (const entry of entries) {
35
35
  if (before !== after) fs.writeFileSync(file, after);
36
36
  console.log(`${entry.split(path.sep).join("/")} VERSION -> ${version}`);
37
37
  }
38
+
39
+ for (const entry of ["plugin.json", path.join(".codex-plugin", "plugin.json")]) {
40
+ const manifestPath = path.join(root, entry);
41
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
42
+ manifest.version = version;
43
+ fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + "\n");
44
+ }
@@ -0,0 +1,19 @@
1
+ ---
2
+ name: codex-bridge
3
+ description: Read saved projects and conversations or delegate authorized work to the user's paired local Codex Desktop through the Codex Bridge plugin.
4
+ ---
5
+
6
+ # Codex Bridge
7
+
8
+ Use this workflow when a user wants ChatGPT to inspect Codex Desktop conversations or send work to their paired computer.
9
+
10
+ 1. Call `codex_bridge_status`. If unpaired or offline, direct the user to the local connector's pairing URL or ask them to start their installed connector. Never claim that an offline computer received work.
11
+ 2. Call `list_codex_projects` or `list_codex_threads` to verify the exact saved project and destination. These calls return queue operation IDs. Use `read_codex_request` to retrieve their results before choosing a destination.
12
+ 3. Create a new conversation with `start_codex_thread` only when the user explicitly or through standing instructions authorizes a new task. Supply the exact saved project path, a clear title, a complete English brief and a fresh UUID `requestId`. Structure briefs as Goal, Context, Task, Scope, Constraints, Done when and Reply format when useful. Preserve literal user text and file names.
13
+ 4. Continue unfinished work in its verified original thread with `send_to_codex_thread`. Read that thread before sending if its identity or current work is uncertain. Supply a fresh UUID per distinct follow-up, retaining the same UUID only for a retry of that exact follow-up.
14
+ 5. Use `read_codex_request` with the returned queue ID. A queued or claimed operation is not a completed Desktop action. The `result` of a completed operation can still indicate a blocked or uncertain Desktop action. Read the actual result before reporting success.
15
+ 6. After a confirmed send, use `read_codex_thread` to inspect progress or replies. Desktop acceptance is separate from completion of the user's task.
16
+
17
+ Never automatically resend work after an uncertain, disconnected or expired claimed operation. Preserve its queue ID, request UUID and any confirmed thread ID. Inspect the original task. An `expired_not_dispatched` request was not taken by the connector. A disconnected computer does not imply that previously dispatched Desktop work stopped.
18
+
19
+ Each ChatGPT account controls only its paired computer. Local allowed roots, saved project identity and original signed-in account bindings are checked by the connector. Do not request a broader root, different account, arbitrary native operation or CLI fallback to bypass a rejection.