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.
Files changed (50) hide show
  1. package/README.md +252 -0
  2. package/dist/attach-manager.js +259 -0
  3. package/dist/bridge.js +931 -0
  4. package/dist/cli-args.js +14 -0
  5. package/dist/cli.js +742 -0
  6. package/dist/codex-prompts.js +148 -0
  7. package/dist/codex-thread-source.js +495 -0
  8. package/dist/codex-transcript.js +415 -0
  9. package/dist/dev-server.js +126 -0
  10. package/dist/discovery.js +111 -0
  11. package/dist/e2e/codec.js +119 -0
  12. package/dist/e2e/crypto.js +127 -0
  13. package/dist/e2e/key-store.js +48 -0
  14. package/dist/e2e/keychain-identity.js +29 -0
  15. package/dist/engine/adapter.js +5 -0
  16. package/dist/engine/claude-adapter.js +77 -0
  17. package/dist/engine/codex-adapter.js +593 -0
  18. package/dist/file-preview.js +292 -0
  19. package/dist/hub-protocol.js +28 -0
  20. package/dist/hub-server.js +106 -0
  21. package/dist/hub.js +84 -0
  22. package/dist/install-util.js +33 -0
  23. package/dist/local-shell.js +32 -0
  24. package/dist/mcp-config.js +230 -0
  25. package/dist/mcp-device-proxy.js +501 -0
  26. package/dist/media-hydrator.js +222 -0
  27. package/dist/message-counter.js +79 -0
  28. package/dist/phone-probe.js +55 -0
  29. package/dist/prompt-detector.js +213 -0
  30. package/dist/protocol.js +3 -0
  31. package/dist/pty-mirror.js +80 -0
  32. package/dist/pty-spawn.js +53 -0
  33. package/dist/scanner.js +422 -0
  34. package/dist/self-update.js +122 -0
  35. package/dist/session-map.js +15 -0
  36. package/dist/session-runner.js +131 -0
  37. package/dist/shell.js +104 -0
  38. package/dist/skills-scanner.js +167 -0
  39. package/dist/stdin-encode.js +32 -0
  40. package/dist/stream-translate.js +122 -0
  41. package/dist/terminal-render.js +29 -0
  42. package/dist/transcript-watcher.js +138 -0
  43. package/dist/transcript.js +346 -0
  44. package/dist/turn-probe.js +152 -0
  45. package/dist/types.js +2 -0
  46. package/dist/watch-manager.js +77 -0
  47. package/install-cc.sh +90 -0
  48. package/install-codex.sh +97 -0
  49. package/package.json +39 -0
  50. 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
+ }