@m2canvas/connect 0.2.0 → 0.3.1
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 +3 -3
- package/dist/cli.js +20 -4
- package/dist/clients.js +22 -10
- package/dist/config-files.js +10 -0
- package/dist/system.js +8 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -10,18 +10,18 @@ It finds the supported clients you have installed, shows what it will change, an
|
|
|
10
10
|
|
|
11
11
|
**In a terminal**, it asks before changing anything and opens each client's browser sign-in, so one command finishes the whole setup.
|
|
12
12
|
|
|
13
|
-
**Run by an agent** (no interactive terminal, e.g. pasted from M2's "Copy to agent"),
|
|
13
|
+
**Run by an agent** (no interactive terminal, e.g. pasted from M2's "Copy to agent"), the agent passes `--client <id>` (`claude-code`, `codex`, `cursor`, `opencode`, `qoder`) so only its own client is touched. It adds the server without asking, skips the sign-ins it can't run there, and prints the remaining steps: a command to run (Codex, Cursor, OpenCode) or a place to click in the app (Claude Code, Qoder). The agent can run the commands or pass the steps on to you. Running it again lists the sign-in steps for clients that were already added.
|
|
14
14
|
|
|
15
15
|
| Client | How it is set up | Sign-in |
|
|
16
16
|
|---|---|---|
|
|
17
17
|
| Claude Code | `claude mcp add --transport http --scope user m2 …` | `claude mcp login m2` |
|
|
18
|
-
| Codex (CLI or the desktop app's bundled CLI) | `
|
|
18
|
+
| Codex (CLI or the desktop app's bundled CLI) | appends `[mcp_servers.m2]` to `~/.codex/config.toml` | `codex mcp login m2` |
|
|
19
19
|
| Cursor | merges `m2` into `~/.cursor/mcp.json`, then `cursor-agent mcp enable m2` | `cursor-agent mcp login m2` |
|
|
20
20
|
| OpenCode | merges `m2` into `~/.config/opencode/opencode.json`, keeping the file's existing format | `opencode mcp auth m2` |
|
|
21
21
|
| Qoder (CLI + desktop app) | `qodercli mcp add -s user -t http m2 …` | in Qoder: Extensions → Connectors → m2 → Authorize |
|
|
22
22
|
|
|
23
23
|
Clients that already have an entry pointing at `mcp.m2ui.ai`, under any name, are skipped. Before a config file is edited, the original is copied to `<file>.bak-m2canvas`.
|
|
24
24
|
|
|
25
|
-
Options: `--dry-run` shows the plan without changing anything; `--yes` skips the confirmation.
|
|
25
|
+
Options: `--client <id>` sets up only that client; `--dry-run` shows the plan without changing anything; `--yes` skips the confirmation.
|
|
26
26
|
|
|
27
27
|
After setup, restart each client so it loads the new server.
|
package/dist/cli.js
CHANGED
|
@@ -8,9 +8,10 @@
|
|
|
8
8
|
// 所以只写入配置,把剩余授权步骤打印成 agent 可以照做 / 转述给用户的清单。
|
|
9
9
|
import { createInterface } from 'node:readline/promises';
|
|
10
10
|
import { M2_MCP_URL, SERVER_NAME } from './config-files.js';
|
|
11
|
-
import { detectClients } from './clients.js';
|
|
11
|
+
import { CLIENTS, detectClients, isClientId } from './clients.js';
|
|
12
12
|
import { run } from './system.js';
|
|
13
|
-
const
|
|
13
|
+
const CLIENT_IDS = Object.keys(CLIENTS).join(', ');
|
|
14
|
+
const HELP = `Usage: npx @m2canvas/connect [--client <id>] [--yes] [--dry-run]
|
|
14
15
|
|
|
15
16
|
Adds M2 Canvas (${M2_MCP_URL}) to every agent client found on this machine
|
|
16
17
|
(Claude Code, Codex, Cursor, OpenCode, Qoder), then signs each one in.
|
|
@@ -19,6 +20,7 @@ In a terminal it asks before changing anything and opens each sign-in in your br
|
|
|
19
20
|
When run by an agent (no terminal), it only adds the server and prints the remaining
|
|
20
21
|
sign-in steps.
|
|
21
22
|
|
|
23
|
+
--client <id> only set up this client (${CLIENT_IDS})
|
|
22
24
|
-y, --yes skip the confirmation prompt
|
|
23
25
|
--dry-run only show what would change
|
|
24
26
|
-h, --help show this help`;
|
|
@@ -59,13 +61,27 @@ function describePending(step) {
|
|
|
59
61
|
return step.manual ?? '';
|
|
60
62
|
}
|
|
61
63
|
async function main() {
|
|
62
|
-
const
|
|
64
|
+
const argv = process.argv.slice(2);
|
|
65
|
+
const args = new Set(argv);
|
|
63
66
|
if (args.has('-h') || args.has('--help')) {
|
|
64
67
|
console.log(HELP);
|
|
65
68
|
return 0;
|
|
66
69
|
}
|
|
67
70
|
const interactive = Boolean(process.stdin.isTTY);
|
|
68
|
-
|
|
71
|
+
// agent 代跑时由起手式带上自己的 --client:只处理这一家,其他客户端不动、不提 ——
|
|
72
|
+
// 用户只是在一个客户端里粘了一段话,不该顺带改掉、也不该被列出别的客户端。
|
|
73
|
+
const clientIndex = argv.indexOf('--client');
|
|
74
|
+
const requested = clientIndex >= 0 ? (argv[clientIndex + 1] ?? '') : null;
|
|
75
|
+
if (requested !== null && !isClientId(requested)) {
|
|
76
|
+
console.error(`Unknown --client "${requested}". Use one of: ${CLIENT_IDS}.`);
|
|
77
|
+
return 2;
|
|
78
|
+
}
|
|
79
|
+
const only = requested ?? undefined;
|
|
80
|
+
const plans = detectClients(only);
|
|
81
|
+
if (only && plans.length === 0) {
|
|
82
|
+
console.log(`Couldn't find a working ${only} CLI on this machine.\nAdd this MCP server by hand instead: ${M2_MCP_URL} (streamable HTTP, OAuth sign-in).`);
|
|
83
|
+
return 1;
|
|
84
|
+
}
|
|
69
85
|
if (plans.length === 0) {
|
|
70
86
|
console.log(`No supported agent client found.\nAdd this MCP server in your client by hand: ${M2_MCP_URL} (streamable HTTP, OAuth sign-in).`);
|
|
71
87
|
return 1;
|
package/dist/clients.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
// 各客户端的探测 / 写入 / 登录。每家的命令与坑位都经本机逐一实测,见 README。
|
|
2
|
-
import { mkdirSync } from 'node:fs';
|
|
3
2
|
import { join } from 'node:path';
|
|
4
|
-
import { M2_MCP_URL, SERVER_NAME, findInClaudeConfig, findInCodexToml, findInQoderSettings, mergeCursorConfig, mergeOpencodeConfig, } from './config-files.js';
|
|
5
|
-
import { existsSync, firstExecutable, firstRunnable, home, readJson, readText, run, which, writeJsonWithBackup } from './system.js';
|
|
3
|
+
import { M2_MCP_URL, SERVER_NAME, findInClaudeConfig, findInCodexToml, findInQoderSettings, appendCodexServer, mergeCursorConfig, mergeOpencodeConfig, } from './config-files.js';
|
|
4
|
+
import { existsSync, firstExecutable, firstRunnable, home, readJson, readText, run, which, writeJsonWithBackup, writeTextWithBackup, } from './system.js';
|
|
6
5
|
function jsonExisting(path, find) {
|
|
7
6
|
const read = readJson(path);
|
|
8
7
|
return read.kind === 'ok' ? find(read.value) : null;
|
|
@@ -34,19 +33,20 @@ function codex() {
|
|
|
34
33
|
if (!bin)
|
|
35
34
|
return null;
|
|
36
35
|
const codexHome = process.env.CODEX_HOME ?? join(home, '.codex');
|
|
37
|
-
const
|
|
36
|
+
const path = join(codexHome, 'config.toml');
|
|
37
|
+
const toml = readText(path);
|
|
38
38
|
return {
|
|
39
39
|
label: 'Codex',
|
|
40
40
|
existing: toml ? findInCodexToml(toml) : null,
|
|
41
41
|
blocked: null,
|
|
42
|
-
actions: [`
|
|
42
|
+
actions: [`add [mcp_servers.${SERVER_NAME}] to ${path}`],
|
|
43
43
|
// 顺序敏感:login 的本地回调端口挂在 login 进程上,必须授权完成后再重启 Codex。
|
|
44
44
|
restart: 'Fully quit and reopen Codex (after signing in).',
|
|
45
45
|
signIn: (_interactive, name) => ({ login: { command: bin, args: ['mcp', 'login', name] }, manual: null }),
|
|
46
46
|
add() {
|
|
47
|
-
//
|
|
48
|
-
|
|
49
|
-
return
|
|
47
|
+
// 直接追加 config.toml,不用 `codex mcp add`(会自己拉起 OAuth 并阻塞,见 appendCodexServer)。
|
|
48
|
+
writeTextWithBackup(path, appendCodexServer(toml));
|
|
49
|
+
return { ok: true };
|
|
50
50
|
},
|
|
51
51
|
};
|
|
52
52
|
}
|
|
@@ -136,6 +136,18 @@ function cursor() {
|
|
|
136
136
|
},
|
|
137
137
|
};
|
|
138
138
|
}
|
|
139
|
-
|
|
140
|
-
|
|
139
|
+
/** `--client` 的取值 → 探测函数。探测会真跑 `--version`,所以按需只跑被点名的那家。 */
|
|
140
|
+
export const CLIENTS = {
|
|
141
|
+
'claude-code': claudeCode,
|
|
142
|
+
codex,
|
|
143
|
+
cursor,
|
|
144
|
+
opencode,
|
|
145
|
+
qoder,
|
|
146
|
+
};
|
|
147
|
+
export function isClientId(value) {
|
|
148
|
+
return Object.hasOwn(CLIENTS, value);
|
|
149
|
+
}
|
|
150
|
+
export function detectClients(only) {
|
|
151
|
+
const ids = only ? [only] : Object.keys(CLIENTS);
|
|
152
|
+
return ids.map((id) => CLIENTS[id]()).filter((plan) => plan !== null);
|
|
141
153
|
}
|
package/dist/config-files.js
CHANGED
|
@@ -88,3 +88,13 @@ export function mergeOpencodeConfig(config) {
|
|
|
88
88
|
: { ...mcp, [SERVER_NAME]: { type: 'remote', url: M2_MCP_URL, enabled: true } };
|
|
89
89
|
return { existing: null, config: { ...base, mcp: nextMcp } };
|
|
90
90
|
}
|
|
91
|
+
/**
|
|
92
|
+
* Codex `config.toml` 追加 m2 段。不走 `codex mcp add`:它在没有已存 token 时会顺手启动 OAuth
|
|
93
|
+
* 并阻塞等浏览器回调(实测),agent 代跑时输出被捕获、链接不可见,命令永远不返回。
|
|
94
|
+
* 只做追加、不解析重写,用户原有内容与注释逐字保留;调用方先用 findInCodexToml 判重。
|
|
95
|
+
*/
|
|
96
|
+
export function appendCodexServer(toml) {
|
|
97
|
+
const base = toml ?? '';
|
|
98
|
+
const separator = base === '' || base.endsWith('\n\n') ? '' : base.endsWith('\n') ? '\n' : '\n\n';
|
|
99
|
+
return `${base}${separator}[mcp_servers.${SERVER_NAME}]\nurl = "${M2_MCP_URL}"\n`;
|
|
100
|
+
}
|
package/dist/system.js
CHANGED
|
@@ -57,16 +57,19 @@ export function readText(path) {
|
|
|
57
57
|
return existsSync(path) ? readFileSync(path, 'utf8') : null;
|
|
58
58
|
}
|
|
59
59
|
/** 覆写前先备份到同目录 `<file>.bak-m2canvas`,返回备份路径(新建文件时为 null)。 */
|
|
60
|
-
export function
|
|
60
|
+
export function writeTextWithBackup(path, text) {
|
|
61
61
|
mkdirSync(dirname(path), { recursive: true });
|
|
62
62
|
let backup = null;
|
|
63
63
|
if (existsSync(path)) {
|
|
64
64
|
backup = `${path}.bak-m2canvas`;
|
|
65
65
|
copyFileSync(path, backup);
|
|
66
66
|
}
|
|
67
|
-
writeFileSync(path,
|
|
67
|
+
writeFileSync(path, text);
|
|
68
68
|
return backup;
|
|
69
69
|
}
|
|
70
|
+
export function writeJsonWithBackup(path, value) {
|
|
71
|
+
return writeTextWithBackup(path, `${JSON.stringify(value, null, 2)}\n`);
|
|
72
|
+
}
|
|
70
73
|
/**
|
|
71
74
|
* 跑子进程。`interactive` 时继承当前终端 —— login / auth 要开浏览器并等回调,
|
|
72
75
|
* Claude Code 的 `mcp login` 还硬性要求 TTY(stdin 不是终端时直接失败),所以必须 inherit。
|
|
@@ -75,6 +78,9 @@ export function run(command, args, interactive = false) {
|
|
|
75
78
|
const result = spawnSync(command, args, {
|
|
76
79
|
stdio: interactive ? 'inherit' : 'pipe',
|
|
77
80
|
encoding: 'utf8',
|
|
81
|
+
// 非交互命令给上限:客户端 CLI 可能在写配置时自己拉起 OAuth 并阻塞等回调(实测 codex mcp add),
|
|
82
|
+
// 输出被 pipe 吞掉时用户看不到链接,没有上限就会永远挂住。交互 login 等用户操作,不设上限。
|
|
83
|
+
timeout: interactive ? undefined : 60_000,
|
|
78
84
|
});
|
|
79
85
|
const output = interactive ? '' : `${result.stdout ?? ''}${result.stderr ?? ''}`.trim();
|
|
80
86
|
return { status: result.error ? null : result.status, output: result.error ? result.error.message : output };
|
package/package.json
CHANGED