cpyany 0.2.7 → 0.2.8
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 +7 -4
- package/index.mjs +97 -14
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -8,8 +8,10 @@ npx cpyany setup
|
|
|
8
8
|
```
|
|
9
9
|
|
|
10
10
|
Auto-detects every supported AI client installed on your machine (Claude Code,
|
|
11
|
-
Claude Desktop, Cursor), opens a browser to authorize, and writes the
|
|
12
|
-
bearer token into each client's MCP config from a single OAuth flow.
|
|
11
|
+
Claude Desktop, Cursor, Codex), opens a browser to authorize, and writes the
|
|
12
|
+
resulting bearer token into each client's MCP config from a single OAuth flow.
|
|
13
|
+
Re-running setup reuses the machine's existing login, so adding a client later
|
|
14
|
+
is just `npx cpyany setup --client <name>`.
|
|
13
15
|
|
|
14
16
|
After setup, ask your coding agent to copy from a reference:
|
|
15
17
|
|
|
@@ -24,6 +26,7 @@ Use cpyany to copy the hero section from https://example.com into this app.
|
|
|
24
26
|
| Claude Code | runs `claude mcp add --scope user --transport http …` (writes `~/.claude.json`) | `~/.claude/rules/cpyany.md` + `~/.claude/skills/cpyany/SKILL.md` | `claude` on `PATH` |
|
|
25
27
|
| Claude Desktop | `~/Library/Application Support/Claude/claude_desktop_config.json` (mac) / `%APPDATA%\Claude\claude_desktop_config.json` (win) — bridged via `mcp-remote` | _(none — Claude Desktop has no rules/skills mechanism)_ | the per-platform Claude config dir exists |
|
|
26
28
|
| Cursor | `~/.cursor/mcp.json` | `~/.cursor/rules/cpyany.mdc` (`alwaysApply: true`) + `~/.cursor/skills/cpyany/SKILL.md` | `~/.cursor` exists |
|
|
29
|
+
| Codex | `[mcp_servers.cpyany]` table in `~/.codex/config.toml` (streamable HTTP, bearer via `http_headers`) | _(none yet — MCP config only)_ | `~/.codex` exists or `codex` on `PATH` |
|
|
27
30
|
|
|
28
31
|
Guidance is two-tier: a short always-loaded rule telling the agent when to use
|
|
29
32
|
cpyany, and a deeper `cpyany` skill with the copy-from-reference workflow.
|
|
@@ -35,8 +38,8 @@ cpyany, and a deeper `cpyany` skill with the copy-from-reference workflow.
|
|
|
35
38
|
npx cpyany setup --client claude-code
|
|
36
39
|
```
|
|
37
40
|
|
|
38
|
-
`--client` accepts `claude-code`, `claude-desktop`, or `
|
|
39
|
-
`--claude` (= `--claude-desktop`), `--code` (= `--claude-code`).
|
|
41
|
+
`--client` accepts `claude-code`, `claude-desktop`, `cursor`, or `codex`.
|
|
42
|
+
Aliases: `--claude` (= `--claude-desktop`), `--code` (= `--claude-code`).
|
|
40
43
|
|
|
41
44
|
## Skip the browser
|
|
42
45
|
|
package/index.mjs
CHANGED
|
@@ -11,7 +11,7 @@ import { execFile } from "node:child_process";
|
|
|
11
11
|
import { promisify } from "node:util";
|
|
12
12
|
import open from "open";
|
|
13
13
|
|
|
14
|
-
const VERSION = "0.2.
|
|
14
|
+
const VERSION = "0.2.8";
|
|
15
15
|
const execFileP = promisify(execFile);
|
|
16
16
|
const APP_URL = process.env.PINGHUMANS_APP_URL ?? "https://pinghumans.com";
|
|
17
17
|
// Hoisted with the other top-of-module consts — the entry try-block runs
|
|
@@ -20,7 +20,7 @@ const APP_URL = process.env.PINGHUMANS_APP_URL ?? "https://pinghumans.com";
|
|
|
20
20
|
// reaches NUDGE_INTERVAL_MS transitively from that path, so it lives here.
|
|
21
21
|
const CLI_CLIENT_ID = "pinghumans-cli";
|
|
22
22
|
const DEVICE_CODE_GRANT = "urn:ietf:params:oauth:grant-type:device_code";
|
|
23
|
-
const SUPPORTED_CLIENTS = ["claude-desktop", "claude-code", "cursor"];
|
|
23
|
+
const SUPPORTED_CLIENTS = ["claude-desktop", "claude-code", "cursor", "codex"];
|
|
24
24
|
// Throttle the passive token-health check to once/day so it neither spams
|
|
25
25
|
// the nudge nor adds a whoami round-trip to every incidental invocation.
|
|
26
26
|
const NUDGE_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
@@ -223,7 +223,7 @@ async function setup(client) {
|
|
|
223
223
|
const detected = await detectClients();
|
|
224
224
|
if (detected.length === 0) {
|
|
225
225
|
throw new Error(
|
|
226
|
-
"Couldn't find Claude Code, Claude Desktop, or
|
|
226
|
+
"Couldn't find Claude Code, Claude Desktop, Cursor, or Codex on this machine. " +
|
|
227
227
|
"Install one of them first, or pass --client to specify manually."
|
|
228
228
|
);
|
|
229
229
|
}
|
|
@@ -244,13 +244,30 @@ async function setup(client) {
|
|
|
244
244
|
}
|
|
245
245
|
}
|
|
246
246
|
|
|
247
|
-
// One
|
|
248
|
-
//
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
_bearerForTelemetry = token;
|
|
247
|
+
// One sign-in regardless of how many configs we'll patch. A stored token
|
|
248
|
+
// that still authenticates is reused — re-running setup to add a second
|
|
249
|
+
// client must not force a fresh sign-in. Anything else falls through to
|
|
250
|
+
// the device flow (RFC 8628 — works over SSH too, no localhost listener).
|
|
252
251
|
const pc0 = await import("picocolors").then((m) => m.default);
|
|
253
|
-
|
|
252
|
+
let token = await resolveLocalToken();
|
|
253
|
+
if (token) {
|
|
254
|
+
try {
|
|
255
|
+
const who = await fetchWhoami(token);
|
|
256
|
+
if (who?.success === false) throw new Error("token revoked");
|
|
257
|
+
const name = who?.email || who?.name;
|
|
258
|
+
console.log(
|
|
259
|
+
`${pc0.green("✔")} Already signed in${name ? ` as ${pc0.bold(name)}` : ""} — reusing this machine's login (\`cpyany remove\` signs out)`
|
|
260
|
+
);
|
|
261
|
+
} catch {
|
|
262
|
+
token = null; // dead or unverifiable → fresh sign-in
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
if (!token) {
|
|
266
|
+
token = await performDeviceLogin();
|
|
267
|
+
if (!token) process.exit(1);
|
|
268
|
+
console.log(`${pc0.green("✔")} Authenticated`);
|
|
269
|
+
}
|
|
270
|
+
_bearerForTelemetry = token;
|
|
254
271
|
await saveLocalToken(token); // lets `cpyany wait`/`whoami` authenticate later
|
|
255
272
|
|
|
256
273
|
// Install everything, then print a Context7-style per-client summary.
|
|
@@ -362,13 +379,14 @@ async function isOnPath(cmd) {
|
|
|
362
379
|
* - claude-desktop: the per-platform config directory exists (created on
|
|
363
380
|
* first launch; absent if the app was never opened)
|
|
364
381
|
* - cursor: ~/.cursor exists
|
|
382
|
+
* - codex: ~/.codex exists, or the `codex` binary is on PATH
|
|
365
383
|
*/
|
|
366
384
|
async function detectClients() {
|
|
367
385
|
const home = homedir();
|
|
368
386
|
const isMac = platform() === "darwin";
|
|
369
387
|
const isWin = platform() === "win32";
|
|
370
388
|
|
|
371
|
-
const [hasClaudeCode, hasClaudeDesktop, hasCursor] = await Promise.all([
|
|
389
|
+
const [hasClaudeCode, hasClaudeDesktop, hasCursor, hasCodexDir, hasCodexBin] = await Promise.all([
|
|
372
390
|
isOnPath("claude"),
|
|
373
391
|
pathExists(
|
|
374
392
|
isMac
|
|
@@ -378,12 +396,15 @@ async function detectClients() {
|
|
|
378
396
|
: join(home, ".config", "Claude")
|
|
379
397
|
),
|
|
380
398
|
pathExists(join(home, ".cursor")),
|
|
399
|
+
pathExists(join(home, ".codex")),
|
|
400
|
+
isOnPath("codex"),
|
|
381
401
|
]);
|
|
382
402
|
|
|
383
403
|
const out = [];
|
|
384
404
|
if (hasClaudeCode) out.push("claude-code");
|
|
385
405
|
if (hasClaudeDesktop) out.push("claude-desktop");
|
|
386
406
|
if (hasCursor) out.push("cursor");
|
|
407
|
+
if (hasCodexDir || hasCodexBin) out.push("codex");
|
|
387
408
|
return out;
|
|
388
409
|
}
|
|
389
410
|
|
|
@@ -580,6 +601,8 @@ function configPath(client) {
|
|
|
580
601
|
}
|
|
581
602
|
case "cursor":
|
|
582
603
|
return join(home, ".cursor", "mcp.json");
|
|
604
|
+
case "codex":
|
|
605
|
+
return join(home, ".codex", "config.toml");
|
|
583
606
|
case "claude-code":
|
|
584
607
|
return null; // managed via `claude mcp add`
|
|
585
608
|
default:
|
|
@@ -591,6 +614,9 @@ async function patchConfig(client, token) {
|
|
|
591
614
|
if (client === "claude-code") {
|
|
592
615
|
return patchClaudeCodeViaCli(token);
|
|
593
616
|
}
|
|
617
|
+
if (client === "codex") {
|
|
618
|
+
return patchCodexConfig(token);
|
|
619
|
+
}
|
|
594
620
|
const path = configPath(client);
|
|
595
621
|
await mkdir(dirname(path), { recursive: true });
|
|
596
622
|
let config = {};
|
|
@@ -637,6 +663,17 @@ async function unpatchConfig(client) {
|
|
|
637
663
|
await execFileP("claude", ["mcp", "remove", "pinghumans", "--scope", "user"]).catch(() => {});
|
|
638
664
|
return;
|
|
639
665
|
}
|
|
666
|
+
if (client === "codex") {
|
|
667
|
+
try {
|
|
668
|
+
const path = configPath("codex");
|
|
669
|
+
const text = await readFile(path, "utf8");
|
|
670
|
+
const next = stripTomlTable(text, "mcp_servers.cpyany");
|
|
671
|
+
if (next !== text) await writeFile(path, next);
|
|
672
|
+
} catch {
|
|
673
|
+
/* nothing to remove */
|
|
674
|
+
}
|
|
675
|
+
return;
|
|
676
|
+
}
|
|
640
677
|
const path = configPath(client);
|
|
641
678
|
try {
|
|
642
679
|
const text = await readFile(path, "utf8");
|
|
@@ -651,6 +688,48 @@ async function unpatchConfig(client) {
|
|
|
651
688
|
}
|
|
652
689
|
}
|
|
653
690
|
|
|
691
|
+
// Codex keeps MCP servers in ~/.codex/config.toml ([mcp_servers.<name>]
|
|
692
|
+
// tables). Streamable HTTP works natively via `url`; the bearer rides in
|
|
693
|
+
// `http_headers` — codex's config validation rejects an inline
|
|
694
|
+
// `bearer_token` key, and the only other option (bearer_token_env_var)
|
|
695
|
+
// needs an env var setup can't persist into the user's shell. Only our
|
|
696
|
+
// own table is rewritten; the rest of the file — users hand-edit
|
|
697
|
+
// config.toml — is left byte-for-byte alone.
|
|
698
|
+
async function patchCodexConfig(token) {
|
|
699
|
+
const path = configPath("codex");
|
|
700
|
+
await mkdir(dirname(path), { recursive: true });
|
|
701
|
+
let text = "";
|
|
702
|
+
try {
|
|
703
|
+
text = await readFile(path, "utf8");
|
|
704
|
+
} catch {
|
|
705
|
+
/* missing — start fresh */
|
|
706
|
+
}
|
|
707
|
+
// Collapse the strip's leftover trailing blank lines so re-runs are
|
|
708
|
+
// byte-stable instead of growing one blank line per invocation.
|
|
709
|
+
text = stripTomlTable(text, "mcp_servers.cpyany").replace(/\n{2,}$/, "\n");
|
|
710
|
+
const block =
|
|
711
|
+
`[mcp_servers.cpyany]\n` +
|
|
712
|
+
`url = "${APP_URL}/api/mcp"\n` +
|
|
713
|
+
`http_headers = { "Authorization" = "Bearer ${token}" }\n`;
|
|
714
|
+
const sep = text.trim().length === 0 ? "" : text.endsWith("\n") ? "\n" : "\n\n";
|
|
715
|
+
await writeFile(path, (text.trim().length === 0 ? "" : text) + sep + block);
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
// Drop one [name] table — its header line plus every line up to the next
|
|
719
|
+
// table header (or EOF). Line-based surgery, not a TOML parse: it only
|
|
720
|
+
// ever deletes between OUR header and the next header, so hand-authored
|
|
721
|
+
// tables around it survive untouched.
|
|
722
|
+
function stripTomlTable(text, tableName) {
|
|
723
|
+
const out = [];
|
|
724
|
+
let inTable = false;
|
|
725
|
+
for (const line of text.split("\n")) {
|
|
726
|
+
const header = /^\s*\[([^\]]+)\]/.exec(line);
|
|
727
|
+
if (header) inTable = header[1].trim() === tableName;
|
|
728
|
+
if (!inTable) out.push(line);
|
|
729
|
+
}
|
|
730
|
+
return out.join("\n");
|
|
731
|
+
}
|
|
732
|
+
|
|
654
733
|
// ─── Agent rules ──────────────────────────────────────────────────────────
|
|
655
734
|
//
|
|
656
735
|
// In addition to MCP tool descriptions, we install a short prose "rule"
|
|
@@ -893,6 +972,7 @@ function parseClient(args) {
|
|
|
893
972
|
if (args[i] === "--code" || args[i] === "--claude-code")
|
|
894
973
|
return "claude-code";
|
|
895
974
|
if (args[i] === "--cursor") return "cursor";
|
|
975
|
+
if (args[i] === "--codex") return "codex";
|
|
896
976
|
}
|
|
897
977
|
return null;
|
|
898
978
|
}
|
|
@@ -903,6 +983,7 @@ function prettyClient(client) {
|
|
|
903
983
|
"claude-desktop": "Claude Desktop",
|
|
904
984
|
"claude-code": "Claude Code",
|
|
905
985
|
cursor: "Cursor",
|
|
986
|
+
codex: "Codex",
|
|
906
987
|
}[client] || client
|
|
907
988
|
);
|
|
908
989
|
}
|
|
@@ -911,8 +992,8 @@ function printHelp() {
|
|
|
911
992
|
console.log(`cpyany — install the cpyany MCP server in your AI client.
|
|
912
993
|
|
|
913
994
|
Usage:
|
|
914
|
-
cpyany setup [--client claude-code|claude-desktop|cursor]
|
|
915
|
-
cpyany remove [--client claude-code|claude-desktop|cursor]
|
|
995
|
+
cpyany setup [--client claude-code|claude-desktop|cursor|codex]
|
|
996
|
+
cpyany remove [--client claude-code|claude-desktop|cursor|codex]
|
|
916
997
|
cpyany wait <ping_id> [--timeout <seconds>]
|
|
917
998
|
# block until the cpyany test has results (exit 0 = news,
|
|
918
999
|
# 2 = timed out). Run it in the background after filing
|
|
@@ -922,8 +1003,10 @@ Usage:
|
|
|
922
1003
|
cpyany version
|
|
923
1004
|
|
|
924
1005
|
Without --client, setup auto-detects every supported AI client installed on
|
|
925
|
-
this machine (Claude Code, Claude Desktop, Cursor) and patches all of
|
|
926
|
-
configs from a single OAuth flow. Use --client to restrict to one.
|
|
1006
|
+
this machine (Claude Code, Claude Desktop, Cursor, Codex) and patches all of
|
|
1007
|
+
their configs from a single OAuth flow. Use --client to restrict to one.
|
|
1008
|
+
Re-running setup reuses this machine's login, so adding a client later is
|
|
1009
|
+
just \`cpyany setup --client <name>\`.
|
|
927
1010
|
|
|
928
1011
|
Setup opens your browser to ${APP_URL}/cli-auth, generates a fresh bearer
|
|
929
1012
|
token after you sign in, and writes it into each client's MCP config.
|