nexting-cc-bridge 0.8.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/README.md +252 -0
- package/dist/attach-manager.js +259 -0
- package/dist/bridge.js +931 -0
- package/dist/cli-args.js +14 -0
- package/dist/cli.js +742 -0
- package/dist/codex-prompts.js +148 -0
- package/dist/codex-thread-source.js +495 -0
- package/dist/codex-transcript.js +415 -0
- package/dist/dev-server.js +126 -0
- package/dist/discovery.js +111 -0
- package/dist/e2e/codec.js +119 -0
- package/dist/e2e/crypto.js +127 -0
- package/dist/e2e/key-store.js +48 -0
- package/dist/e2e/keychain-identity.js +29 -0
- package/dist/engine/adapter.js +5 -0
- package/dist/engine/claude-adapter.js +77 -0
- package/dist/engine/codex-adapter.js +593 -0
- package/dist/file-preview.js +292 -0
- package/dist/hub-protocol.js +28 -0
- package/dist/hub-server.js +106 -0
- package/dist/hub.js +84 -0
- package/dist/install-util.js +33 -0
- package/dist/local-shell.js +32 -0
- package/dist/mcp-config.js +230 -0
- package/dist/mcp-device-proxy.js +501 -0
- package/dist/media-hydrator.js +222 -0
- package/dist/message-counter.js +79 -0
- package/dist/phone-probe.js +55 -0
- package/dist/prompt-detector.js +213 -0
- package/dist/protocol.js +3 -0
- package/dist/pty-mirror.js +80 -0
- package/dist/pty-spawn.js +53 -0
- package/dist/scanner.js +422 -0
- package/dist/self-update.js +122 -0
- package/dist/session-map.js +15 -0
- package/dist/session-runner.js +131 -0
- package/dist/shell.js +104 -0
- package/dist/skills-scanner.js +167 -0
- package/dist/stdin-encode.js +32 -0
- package/dist/stream-translate.js +122 -0
- package/dist/terminal-render.js +29 -0
- package/dist/transcript-watcher.js +138 -0
- package/dist/transcript.js +346 -0
- package/dist/turn-probe.js +152 -0
- package/dist/types.js +2 -0
- package/dist/watch-manager.js +77 -0
- package/install-cc.sh +90 -0
- package/install-codex.sh +97 -0
- package/package.json +39 -0
- package/shim/claude +55 -0
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
// Per-session MCP config writer for the device-tool proxy.
|
|
2
|
+
//
|
|
3
|
+
// When the bridge spawns an engine child (claude / codex) for an attached
|
|
4
|
+
// session it injects the `pinclaw-device` MCP server so the agent can call the
|
|
5
|
+
// phone's device tools (calendar / reminders / contacts / location / health /
|
|
6
|
+
// homekit). The proxy (`mcp-device-proxy.ts`) is a standalone stdio MCP server
|
|
7
|
+
// that forwards each call to the cloud, which fans it out to the phone's SSE
|
|
8
|
+
// stream. This module ONLY writes / removes the per-session config that points
|
|
9
|
+
// the engine at that proxy.
|
|
10
|
+
//
|
|
11
|
+
// claude: a JSON file at `~/.nexting/cc-mcp-{sessionId}.json`, passed via
|
|
12
|
+
// `--mcp-config <path>` (verified supported in `--print` mode, CLI 2.1.170 —
|
|
13
|
+
// `--mcp-config <configs...>` is a global flag, not interactive-only; chosen
|
|
14
|
+
// over writing `.mcp.json` into the user's repo so we never touch the project).
|
|
15
|
+
// codex: app-server reads `~/.codex/config.toml` `[mcp_servers.*]`; we MERGE a
|
|
16
|
+
// marker-fenced `[mcp_servers.pinclaw-device-{sessionId}]` block in/out
|
|
17
|
+
// idempotently, leaving the rest of the (rich, user-owned) file byte-intact.
|
|
18
|
+
//
|
|
19
|
+
// The proxy reads its config from env (see mcp-device-proxy.ts):
|
|
20
|
+
// NEXTING_CLOUD_URL / NEXTING_BUS_TOKEN / NEXTING_ENGINE(cc|codex) /
|
|
21
|
+
// NEXTING_SESSION_ID
|
|
22
|
+
import fs from "node:fs";
|
|
23
|
+
import os from "node:os";
|
|
24
|
+
import path from "node:path";
|
|
25
|
+
import { fileURLToPath } from "node:url";
|
|
26
|
+
/** Map the bridge's engine label to the proxy's NEXTING_ENGINE value. */
|
|
27
|
+
export function proxyEngine(engine) {
|
|
28
|
+
return engine === "codex" ? "codex" : "cc";
|
|
29
|
+
}
|
|
30
|
+
/** Absolute path to the compiled proxy entrypoint. The bridge runs from
|
|
31
|
+
* `dist/<module>.js`, so the proxy sits at `dist/mcp-device-proxy.js` next to
|
|
32
|
+
* this module. Under tsx (dev) it resolves to the `.ts` source instead — node
|
|
33
|
+
* runs either via the shebang-less `node <path>` spawn, and tsx is already the
|
|
34
|
+
* loader for the dev path, so both work. */
|
|
35
|
+
export function resolveProxyEntry() {
|
|
36
|
+
const here = fileURLToPath(import.meta.url); // .../dist/mcp-config.js (or src/mcp-config.ts)
|
|
37
|
+
return path.join(path.dirname(here), "mcp-device-proxy.js");
|
|
38
|
+
}
|
|
39
|
+
const NEXTING_DIR = path.join(os.homedir(), ".nexting");
|
|
40
|
+
/** Per-session claude MCP config file path. */
|
|
41
|
+
export function claudeMcpConfigPath(sessionId) {
|
|
42
|
+
return path.join(NEXTING_DIR, `cc-mcp-${sanitizeId(sessionId)}.json`);
|
|
43
|
+
}
|
|
44
|
+
/** The codex config.toml the app-server reads. */
|
|
45
|
+
export function codexConfigTomlPath() {
|
|
46
|
+
return path.join(os.homedir(), ".codex", "config.toml");
|
|
47
|
+
}
|
|
48
|
+
/** Server key under `[mcp_servers.*]` for a session (codex). TOML bare keys
|
|
49
|
+
* allow `[A-Za-z0-9_-]`; we sanitize the sessionId to stay in that set. */
|
|
50
|
+
export function codexServerKey(sessionId) {
|
|
51
|
+
return `pinclaw-device-${sanitizeId(sessionId)}`;
|
|
52
|
+
}
|
|
53
|
+
/** Keep ids filesystem- and TOML-key-safe (uuids + "new" already are, but a
|
|
54
|
+
* resumed-session id could in theory carry odd chars). */
|
|
55
|
+
function sanitizeId(id) {
|
|
56
|
+
return id.replace(/[^A-Za-z0-9_-]/g, "_");
|
|
57
|
+
}
|
|
58
|
+
// --- the env the proxy needs, shared by both engines -------------------------
|
|
59
|
+
function proxyEnv(input) {
|
|
60
|
+
return {
|
|
61
|
+
NEXTING_CLOUD_URL: input.cloudUrl,
|
|
62
|
+
NEXTING_BUS_TOKEN: input.busToken,
|
|
63
|
+
NEXTING_ENGINE: proxyEngine(input.engine),
|
|
64
|
+
// Route to the real session id if known (post-rekey), else the file key.
|
|
65
|
+
NEXTING_SESSION_ID: input.routeSessionId ?? input.sessionId,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
// --- claude (JSON file) ------------------------------------------------------
|
|
69
|
+
/** Build the claude `--mcp-config` JSON object (exported for tests). */
|
|
70
|
+
export function buildClaudeMcpConfig(input) {
|
|
71
|
+
return {
|
|
72
|
+
mcpServers: {
|
|
73
|
+
"pinclaw-device": {
|
|
74
|
+
type: "stdio",
|
|
75
|
+
command: process.execPath, // the node running the bridge (launchd PATH is minimal)
|
|
76
|
+
args: [resolveProxyEntry()],
|
|
77
|
+
env: proxyEnv(input),
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
/** Write the per-session claude MCP config file. Returns its path (to pass as
|
|
83
|
+
* `--mcp-config <path>`). Never throws on a transient write error — a failed
|
|
84
|
+
* config must not block the session; it just means no device tools. */
|
|
85
|
+
export function writeClaudeMcpConfig(input) {
|
|
86
|
+
const p = claudeMcpConfigPath(input.sessionId);
|
|
87
|
+
try {
|
|
88
|
+
fs.mkdirSync(NEXTING_DIR, { recursive: true });
|
|
89
|
+
fs.writeFileSync(p, JSON.stringify(buildClaudeMcpConfig(input), null, 2));
|
|
90
|
+
return p;
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/** Remove the per-session claude MCP config file (session end / cleanup). */
|
|
97
|
+
export function removeClaudeMcpConfig(sessionId) {
|
|
98
|
+
try {
|
|
99
|
+
fs.unlinkSync(claudeMcpConfigPath(sessionId));
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
/* already gone */
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
// --- codex (config.toml merge) -----------------------------------------------
|
|
106
|
+
/** TOML-escape a basic string value (double-quoted). */
|
|
107
|
+
function tomlString(value) {
|
|
108
|
+
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
109
|
+
}
|
|
110
|
+
const FENCE_START = (key) => `# >>> pinclaw-device ${key} >>>`;
|
|
111
|
+
const FENCE_END = (key) => `# <<< pinclaw-device ${key} <<<`;
|
|
112
|
+
/** Build the fenced `[mcp_servers.<key>]` block for codex (exported for tests).
|
|
113
|
+
* Fenced so it can be removed idempotently without a TOML parser. */
|
|
114
|
+
export function buildCodexMcpBlock(input) {
|
|
115
|
+
const key = codexServerKey(input.sessionId);
|
|
116
|
+
const env = proxyEnv(input);
|
|
117
|
+
const envLines = Object.entries(env)
|
|
118
|
+
.map(([k, v]) => `${k} = ${tomlString(v)}`)
|
|
119
|
+
.join("\n");
|
|
120
|
+
return [
|
|
121
|
+
FENCE_START(key),
|
|
122
|
+
`[mcp_servers.${key}]`,
|
|
123
|
+
`command = ${tomlString(process.execPath)}`,
|
|
124
|
+
`args = [${tomlString(resolveProxyEntry())}]`,
|
|
125
|
+
``,
|
|
126
|
+
`[mcp_servers.${key}.env]`,
|
|
127
|
+
envLines,
|
|
128
|
+
FENCE_END(key),
|
|
129
|
+
].join("\n");
|
|
130
|
+
}
|
|
131
|
+
/** Strip a previously-written fenced block for `key` out of `toml` content.
|
|
132
|
+
* Pure (exported for tests) — handles missing block (no-op) and trailing
|
|
133
|
+
* newline hygiene so repeated write/remove cycles don't accrete blank lines. */
|
|
134
|
+
export function stripCodexMcpBlock(toml, key) {
|
|
135
|
+
const start = FENCE_START(key);
|
|
136
|
+
const end = FENCE_END(key);
|
|
137
|
+
const lines = toml.split("\n");
|
|
138
|
+
const out = [];
|
|
139
|
+
let skipping = false;
|
|
140
|
+
for (const line of lines) {
|
|
141
|
+
if (line.trim() === start) {
|
|
142
|
+
skipping = true;
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
if (skipping) {
|
|
146
|
+
if (line.trim() === end)
|
|
147
|
+
skipping = false;
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
out.push(line);
|
|
151
|
+
}
|
|
152
|
+
// collapse any double-blank seam left where the block was removed
|
|
153
|
+
return out.join("\n").replace(/\n{3,}/g, "\n\n");
|
|
154
|
+
}
|
|
155
|
+
/** Merge the codex MCP block into `existing` toml content (pure, for tests).
|
|
156
|
+
* Idempotent: strips any prior block for this session first, then appends the
|
|
157
|
+
* fresh one (token/sessionId may have changed across reconnects). */
|
|
158
|
+
export function mergeCodexMcpToml(existing, input) {
|
|
159
|
+
const key = codexServerKey(input.sessionId);
|
|
160
|
+
const base = stripCodexMcpBlock(existing, key).replace(/\s+$/, "");
|
|
161
|
+
const block = buildCodexMcpBlock(input);
|
|
162
|
+
return base.length > 0 ? `${base}\n\n${block}\n` : `${block}\n`;
|
|
163
|
+
}
|
|
164
|
+
/** Merge the pinclaw-device server into the codex config.toml on disk. Never
|
|
165
|
+
* throws — a failed merge just means no device tools for this session. */
|
|
166
|
+
export function writeCodexMcpConfig(input) {
|
|
167
|
+
const p = codexConfigTomlPath();
|
|
168
|
+
try {
|
|
169
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
170
|
+
let existing = "";
|
|
171
|
+
try {
|
|
172
|
+
existing = fs.readFileSync(p, "utf8");
|
|
173
|
+
}
|
|
174
|
+
catch {
|
|
175
|
+
/* new file */
|
|
176
|
+
}
|
|
177
|
+
fs.writeFileSync(p, mergeCodexMcpToml(existing, input));
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
/* leave config untouched on error */
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
/** Remove this session's pinclaw-device block from the codex config.toml. */
|
|
184
|
+
export function removeCodexMcpConfig(sessionId) {
|
|
185
|
+
const p = codexConfigTomlPath();
|
|
186
|
+
try {
|
|
187
|
+
const existing = fs.readFileSync(p, "utf8");
|
|
188
|
+
const stripped = stripCodexMcpBlock(existing, codexServerKey(sessionId));
|
|
189
|
+
if (stripped !== existing)
|
|
190
|
+
fs.writeFileSync(p, stripped);
|
|
191
|
+
}
|
|
192
|
+
catch {
|
|
193
|
+
/* nothing to strip */
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
// --- unified write / cleanup -------------------------------------------------
|
|
197
|
+
/** Write the per-session MCP config for the given engine. For claude, returns
|
|
198
|
+
* the config file path to append as `--mcp-config <path>`; for codex returns
|
|
199
|
+
* null (the config lives in the shared config.toml the app-server reads). */
|
|
200
|
+
export function writeMcpConfig(input) {
|
|
201
|
+
if (input.engine === "codex") {
|
|
202
|
+
writeCodexMcpConfig(input);
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
return writeClaudeMcpConfig(input);
|
|
206
|
+
}
|
|
207
|
+
/** Tear down the per-session MCP config (session end). Engine-aware. */
|
|
208
|
+
export function cleanupMcpConfig(engine, sessionId) {
|
|
209
|
+
if (engine === "codex")
|
|
210
|
+
removeCodexMcpConfig(sessionId);
|
|
211
|
+
else
|
|
212
|
+
removeClaudeMcpConfig(sessionId);
|
|
213
|
+
}
|
|
214
|
+
/** Derive the cloud HTTP base from the bridge's WS connect url.
|
|
215
|
+
* `wss://api.nexting.ai/cc-bridge/connect` → `https://api.nexting.ai`. */
|
|
216
|
+
export function cloudHttpBaseFromWsUrl(wsUrl) {
|
|
217
|
+
try {
|
|
218
|
+
const u = new URL(wsUrl);
|
|
219
|
+
const proto = u.protocol === "ws:" ? "http:" : "https:";
|
|
220
|
+
return `${proto}//${u.host}`;
|
|
221
|
+
}
|
|
222
|
+
catch {
|
|
223
|
+
// Fallback: best-effort scheme swap + strip the path.
|
|
224
|
+
return wsUrl
|
|
225
|
+
.replace(/^wss:/, "https:")
|
|
226
|
+
.replace(/^ws:/, "http:")
|
|
227
|
+
.replace(/\/cc-bridge\/connect.*$/, "")
|
|
228
|
+
.replace(/\/codex-bridge\/connect.*$/, "");
|
|
229
|
+
}
|
|
230
|
+
}
|