patchxnote-agent 0.2.6 → 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 +41 -7
- package/bin/patchxnote-agent.js +430 -34
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,26 +2,60 @@
|
|
|
2
2
|
|
|
3
3
|
[English README](https://github.com/ZsTs119/patchxnote-agent#readme) | [简体中文说明](https://github.com/ZsTs119/patchxnote-agent/blob/main/README.zh-CN.md) | [飞书公开使用指南](https://patchx2025.feishu.cn/wiki/PnVRwYT7IirFPckairGcWPnHnCd)
|
|
4
4
|
|
|
5
|
-
This npm package is the installer wrapper for PatchXNote Agent. It downloads the matching native `patchxnote` CLI binary from GitHub Releases, verifies `checksums.txt`, and installs it into a user-writable directory.
|
|
5
|
+
This npm package is the installer and thin launcher wrapper for PatchXNote Agent. It downloads or verifies the matching native `patchxnote` CLI binary from GitHub Releases, verifies `checksums.txt`, and installs it into a user-writable directory before delegating runtime commands to that binary.
|
|
6
6
|
|
|
7
7
|
PatchXNote Agent is the local AI assistant connector for PatchXNote. It lets an MCP-capable AI assistant find PatchXNote records, inspect AI-generated results, create Markdown drafts, and manually send user-approved messages to Feishu, DingTalk, or another webhook.
|
|
8
8
|
|
|
9
9
|
Give this one-line prompt to a local-command-capable AI assistant:
|
|
10
10
|
|
|
11
11
|
```text
|
|
12
|
-
|
|
12
|
+
Connect PatchXNote Agent by following the public guide at https://patchx2025.feishu.cn/wiki/PnVRwYT7IirFPckairGcWPnHnCd: run npx -y patchxnote-agent@latest setup --client <my-client>, keep MCP config secret-free, and guide me through login in the same runtime that will launch the MCP server. Do not ask me to paste OTP codes, access tokens, or refresh tokens into chat. GitHub repository: https://github.com/ZsTs119/patchxnote-agent
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
Run setup for a supported local client:
|
|
16
16
|
|
|
17
17
|
```sh
|
|
18
|
-
npx -y patchxnote-agent@
|
|
18
|
+
npx -y patchxnote-agent@latest setup --client cursor
|
|
19
|
+
npx -y patchxnote-agent@latest setup --client vscode
|
|
20
|
+
npx -y patchxnote-agent@latest setup --client codex
|
|
19
21
|
```
|
|
20
22
|
|
|
21
|
-
|
|
23
|
+
Log in with browser OAuth, then check or clear the MCP login state:
|
|
22
24
|
|
|
23
25
|
```sh
|
|
24
|
-
patchxnote mcp
|
|
26
|
+
npx -y patchxnote-agent@latest mcp login
|
|
27
|
+
npx -y patchxnote-agent@latest mcp status
|
|
28
|
+
npx -y patchxnote-agent@latest mcp logout --local-only
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Print the generic MCP config manually:
|
|
32
|
+
|
|
33
|
+
```sh
|
|
34
|
+
npx -y patchxnote-agent@latest mcp config
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
The generated config uses this universal local stdio MCP command:
|
|
38
|
+
|
|
39
|
+
```sh
|
|
40
|
+
npx -y patchxnote-agent@latest mcp serve
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
The older terminal Agent login remains available for legacy local CLI/MCP fallback:
|
|
44
|
+
|
|
45
|
+
```sh
|
|
46
|
+
npx -y patchxnote-agent@latest login
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
`setup` reuses the same `mcp login` browser OAuth flow. The GoServer website owns phone OTP input; the local Agent owns the loopback callback, token exchange, secure storage, and stdio bridge. `mcp serve` never opens a browser during editor startup. It stores credentials in the OS-native keychain and keeps MCP config free of phone numbers, OTP codes, access tokens, and refresh tokens.
|
|
50
|
+
|
|
51
|
+
P0 local client IDs: `vscode`, `cursor`, `codex`, `claude-code`, `claude-desktop`, `windsurf`, `trae`, `qoder`, `workbuddy`.
|
|
52
|
+
|
|
53
|
+
Platform clients such as Feishu Aily, Doubao Work Partner, Tencent Agent Development Platform, and enterprise WorkBuddy need the hosted remote MCP gateway plus platform-console acceptance; local `npx` setup is for desktop and terminal clients.
|
|
54
|
+
|
|
55
|
+
If a client rejects `npx` or times out during first binary download, run the stable absolute-path fallback:
|
|
56
|
+
|
|
57
|
+
```sh
|
|
58
|
+
npx -y patchxnote-agent@latest install --print-config
|
|
25
59
|
```
|
|
26
60
|
|
|
27
61
|
It currently exposes 19 MCP tools grouped around account/record lookup, webhook configuration and sending, and explicit AI result inspection.
|
|
@@ -38,7 +72,7 @@ patchxnote webhook send --target "Product Feishu" --file ./message.md
|
|
|
38
72
|
|
|
39
73
|
Webhook aliases can contain Chinese text, spaces, and dots.
|
|
40
74
|
|
|
41
|
-
Server-backed PatchXNote data access remains read-only through dedicated `/v1/agent/**` APIs. Record lookup can include formal saved results and readable model-generated outputs returned by the server. Local webhook tools can configure named targets and perform explicit manual sends. AI result tools can inspect source text, AI response, parsed result, and final result when explicitly called. The CLI stores credentials and webhook secrets in the OS-native keychain when available, never lists webhook URLs or signing secrets back, and does not expose raw audio, audio downloads, hardware write actions, payment flows, or Admin APIs.
|
|
75
|
+
Server-backed PatchXNote data access remains read-only through dedicated `/v1/agent/**` APIs. Record lookup can include formal saved results and readable model-generated outputs returned by the server. Local webhook tools can configure named targets and perform explicit manual sends. AI result tools can inspect source text, AI response, parsed result, and final result when explicitly called. MCP config contains no phone number, OTP, access token, refresh token, webhook secret, or base URL by default. The CLI stores credentials and webhook secrets in the OS-native keychain when available, never lists webhook URLs or signing secrets back, and does not expose raw audio, audio downloads, hardware write actions, payment flows, or Admin APIs.
|
|
42
76
|
|
|
43
77
|
For full installation, MCP setup, security notes, and troubleshooting, read the GitHub documentation:
|
|
44
78
|
|
package/bin/patchxnote-agent.js
CHANGED
|
@@ -6,19 +6,110 @@ const fs = require("fs");
|
|
|
6
6
|
const https = require("https");
|
|
7
7
|
const os = require("os");
|
|
8
8
|
const path = require("path");
|
|
9
|
+
const { spawn, spawnSync } = require("child_process");
|
|
9
10
|
|
|
10
11
|
const packageRoot = path.resolve(__dirname, "..");
|
|
11
12
|
const packageJSON = require(path.join(packageRoot, "package.json"));
|
|
12
13
|
const repo = "ZsTs119/patchxnote-agent";
|
|
14
|
+
const lockTimeoutMs = 10000;
|
|
15
|
+
const staleLockMs = 10 * 60 * 1000;
|
|
13
16
|
|
|
14
17
|
async function main(argv) {
|
|
15
18
|
const parsed = parseArgs(argv);
|
|
16
|
-
|
|
17
|
-
|
|
19
|
+
|
|
20
|
+
if (parsed.command === "install" || parsed.command === "update") {
|
|
21
|
+
await runInstall(parsed);
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
if (parsed.command === "uninstall") {
|
|
25
|
+
await runUninstall(parsed);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
if (parsed.command === "login") {
|
|
29
|
+
await runLogin(parsed);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
if (parsed.command === "setup") {
|
|
33
|
+
await runSetup(parsed);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
if (parsed.command === "mcp" && parsed.subcommand === "config") {
|
|
37
|
+
printUniversalMCPConfig();
|
|
38
|
+
return;
|
|
18
39
|
}
|
|
40
|
+
if (parsed.command === "mcp" && ["login", "status", "logout"].includes(parsed.subcommand)) {
|
|
41
|
+
await runMCPSubcommand(parsed);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
if (parsed.command === "mcp" && parsed.subcommand === "serve") {
|
|
45
|
+
await runMCPServe(parsed);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
throw new Error(usage());
|
|
50
|
+
}
|
|
19
51
|
|
|
20
|
-
|
|
21
|
-
const
|
|
52
|
+
async function runInstall(parsed) {
|
|
53
|
+
const plan = createInstallPlan(parsed.command, parsed.options);
|
|
54
|
+
if (parsed.options.dryRun) {
|
|
55
|
+
printPlan(plan);
|
|
56
|
+
if (parsed.options.printConfig) {
|
|
57
|
+
printMCPConfig(plan.install_path);
|
|
58
|
+
}
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
await withInstallLock(plan, async () => installBinary(plan, parsed.options));
|
|
62
|
+
console.log(`Installed PatchXNote Agent ${plan.version} to ${plan.install_path}`);
|
|
63
|
+
printPathGuidance(plan.install_dir, plan.platform);
|
|
64
|
+
if (parsed.options.printConfig) {
|
|
65
|
+
printMCPConfig(plan.install_path);
|
|
66
|
+
} else {
|
|
67
|
+
console.log("Run: patchxnote login");
|
|
68
|
+
console.log("MCP config: patchxnote mcp serve");
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function runUninstall(parsed) {
|
|
73
|
+
const plan = createInstallPlan(parsed.command, parsed.options);
|
|
74
|
+
await uninstall(plan, parsed.options);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function runLogin(parsed) {
|
|
78
|
+
const plan = await ensureBinary(parsed.options, { stderr: process.stderr });
|
|
79
|
+
const code = await spawnAndWait(plan.install_path, ["login", ...parsed.passthroughArgs], {
|
|
80
|
+
stdio: "inherit"
|
|
81
|
+
});
|
|
82
|
+
process.exitCode = code;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function runSetup(parsed) {
|
|
86
|
+
const plan = await ensureBinary(parsed.options, { stderr: process.stderr });
|
|
87
|
+
const code = await spawnAndWait(plan.install_path, ["setup", ...parsed.passthroughArgs], {
|
|
88
|
+
stdio: "inherit"
|
|
89
|
+
});
|
|
90
|
+
process.exitCode = code;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function runMCPServe(parsed) {
|
|
94
|
+
const plan = await ensureBinary(parsed.options, { stderr: process.stderr });
|
|
95
|
+
const code = await spawnAndWait(plan.install_path, ["mcp", "serve", ...parsed.passthroughArgs], {
|
|
96
|
+
stdio: ["pipe", "inherit", "inherit"],
|
|
97
|
+
pipeStdin: true
|
|
98
|
+
});
|
|
99
|
+
process.exitCode = code;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function runMCPSubcommand(parsed) {
|
|
103
|
+
const plan = await ensureBinary(parsed.options, { stderr: process.stderr });
|
|
104
|
+
const code = await spawnAndWait(plan.install_path, ["mcp", parsed.subcommand, ...parsed.passthroughArgs], {
|
|
105
|
+
stdio: "inherit"
|
|
106
|
+
});
|
|
107
|
+
process.exitCode = code;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function createInstallPlan(command, options = {}) {
|
|
111
|
+
const target = resolveTarget(options.platform || process.platform, options.arch || process.arch);
|
|
112
|
+
const installDir = options.installDir || defaultInstallDir(process.platform);
|
|
22
113
|
const binaryName = target.platform === "windows" ? "patchxnote.exe" : "patchxnote";
|
|
23
114
|
const installPath = joinInstallPath(installDir, binaryName, target.platform);
|
|
24
115
|
const version = packageJSON.version;
|
|
@@ -37,48 +128,65 @@ async function main(argv) {
|
|
|
37
128
|
install_path: installPath,
|
|
38
129
|
install_dir_on_path: isInstallDirOnPath(installDir, process.env.PATH || "", target.platform),
|
|
39
130
|
path_hint: pathHint(installDir, target.platform),
|
|
131
|
+
asset_name: assetName,
|
|
40
132
|
asset_url: assetURL,
|
|
41
133
|
checksums_url: checksumsURL,
|
|
42
|
-
action:
|
|
43
|
-
dry_run: Boolean(
|
|
134
|
+
action: command,
|
|
135
|
+
dry_run: Boolean(options.dryRun)
|
|
44
136
|
};
|
|
137
|
+
return plan;
|
|
138
|
+
}
|
|
45
139
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
140
|
+
async function installBinary(plan, options = {}) {
|
|
141
|
+
let binary;
|
|
142
|
+
if (options.fromLocal) {
|
|
143
|
+
binary = await fs.promises.readFile(options.fromLocal);
|
|
144
|
+
} else {
|
|
145
|
+
binary = await download(plan.asset_url);
|
|
146
|
+
const checksums = await download(plan.checksums_url, "utf8");
|
|
147
|
+
verifyChecksum(binary, plan.asset_name, checksums);
|
|
49
148
|
}
|
|
50
149
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
150
|
+
await fs.promises.mkdir(plan.install_dir, { recursive: true, mode: 0o755 });
|
|
151
|
+
const tempPath = `${plan.install_path}.tmp-${process.pid}-${Date.now()}`;
|
|
152
|
+
try {
|
|
153
|
+
await fs.promises.writeFile(tempPath, binary, { mode: plan.platform === "windows" ? 0o644 : 0o755 });
|
|
154
|
+
if (plan.platform !== "windows") {
|
|
155
|
+
await fs.promises.chmod(tempPath, 0o755);
|
|
156
|
+
await assertExecutable(tempPath);
|
|
55
157
|
}
|
|
56
|
-
|
|
158
|
+
await replaceInstalledBinary(tempPath, plan.install_path);
|
|
159
|
+
if (plan.platform !== "windows") {
|
|
160
|
+
await assertExecutable(plan.install_path);
|
|
161
|
+
}
|
|
162
|
+
} finally {
|
|
163
|
+
await fs.promises.rm(tempPath, { force: true }).catch(() => {});
|
|
57
164
|
}
|
|
165
|
+
}
|
|
58
166
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
167
|
+
async function replaceInstalledBinary(tempPath, installPath) {
|
|
168
|
+
const backupPath = `${installPath}.bak-${process.pid}-${Date.now()}`;
|
|
169
|
+
let hasBackup = false;
|
|
170
|
+
try {
|
|
171
|
+
await fs.promises.rename(installPath, backupPath);
|
|
172
|
+
hasBackup = true;
|
|
173
|
+
} catch (error) {
|
|
174
|
+
if (error.code !== "ENOENT") {
|
|
175
|
+
throw error;
|
|
176
|
+
}
|
|
66
177
|
}
|
|
67
178
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
179
|
+
try {
|
|
180
|
+
await fs.promises.rename(tempPath, installPath);
|
|
181
|
+
} catch (error) {
|
|
182
|
+
if (hasBackup) {
|
|
183
|
+
await fs.promises.rename(backupPath, installPath).catch(() => {});
|
|
184
|
+
}
|
|
185
|
+
throw error;
|
|
73
186
|
}
|
|
74
187
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
if (parsed.options.printConfig) {
|
|
78
|
-
printMCPConfig(installPath);
|
|
79
|
-
} else {
|
|
80
|
-
console.log("Run: patchxnote login");
|
|
81
|
-
console.log("MCP config: patchxnote mcp serve");
|
|
188
|
+
if (hasBackup) {
|
|
189
|
+
await fs.promises.rm(backupPath, { force: true }).catch(() => {});
|
|
82
190
|
}
|
|
83
191
|
}
|
|
84
192
|
|
|
@@ -98,8 +206,218 @@ async function assertExecutable(installPath) {
|
|
|
98
206
|
}
|
|
99
207
|
}
|
|
100
208
|
|
|
209
|
+
async function ensureBinary(options = {}, io = {}) {
|
|
210
|
+
const plan = createInstallPlan("install", options);
|
|
211
|
+
return withInstallLock(plan, async () => {
|
|
212
|
+
const inspected = inspectInstalledBinary(plan);
|
|
213
|
+
if (inspected.ok && inspected.version === plan.version) {
|
|
214
|
+
return plan;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (!inspected.exists) {
|
|
218
|
+
writeDiagnostic(io, `PatchXNote Agent binary missing; installing ${plan.version}.\n`);
|
|
219
|
+
} else if (inspected.ok) {
|
|
220
|
+
writeDiagnostic(io, `PatchXNote Agent binary version ${inspected.version} does not match package ${plan.version}; reinstalling.\n`);
|
|
221
|
+
} else {
|
|
222
|
+
writeDiagnostic(io, `PatchXNote Agent binary preflight failed; reinstalling ${plan.version}.\n`);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
await installBinary(plan, options);
|
|
226
|
+
const afterInstall = inspectInstalledBinary(plan);
|
|
227
|
+
if (!afterInstall.ok) {
|
|
228
|
+
throw new Error(`installed PatchXNote Agent binary failed preflight: ${afterInstall.reason}`);
|
|
229
|
+
}
|
|
230
|
+
if (afterInstall.version !== plan.version) {
|
|
231
|
+
throw new Error(`installed PatchXNote Agent binary version ${afterInstall.version} does not match package ${plan.version}`);
|
|
232
|
+
}
|
|
233
|
+
return plan;
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function inspectInstalledBinary(plan) {
|
|
238
|
+
if (!fs.existsSync(plan.install_path)) {
|
|
239
|
+
return { exists: false, ok: false, reason: "missing" };
|
|
240
|
+
}
|
|
241
|
+
const result = spawnSync(plan.install_path, ["version", "--output", "json"], {
|
|
242
|
+
encoding: "utf8",
|
|
243
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
244
|
+
timeout: 5000,
|
|
245
|
+
windowsHide: true,
|
|
246
|
+
maxBuffer: 1024 * 1024
|
|
247
|
+
});
|
|
248
|
+
if (result.error) {
|
|
249
|
+
return { exists: true, ok: false, reason: result.error.message };
|
|
250
|
+
}
|
|
251
|
+
if (result.status !== 0) {
|
|
252
|
+
return { exists: true, ok: false, reason: `exit ${result.status}` };
|
|
253
|
+
}
|
|
254
|
+
let decoded;
|
|
255
|
+
try {
|
|
256
|
+
decoded = JSON.parse(result.stdout.trim());
|
|
257
|
+
} catch (error) {
|
|
258
|
+
return { exists: true, ok: false, reason: "invalid version output" };
|
|
259
|
+
}
|
|
260
|
+
return { exists: true, ok: true, version: String(decoded.version || "") };
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
async function withInstallLock(plan, action) {
|
|
264
|
+
await fs.promises.mkdir(plan.install_dir, { recursive: true, mode: 0o755 });
|
|
265
|
+
const release = await acquireInstallLock(`${plan.install_path}.lock`);
|
|
266
|
+
try {
|
|
267
|
+
return await action();
|
|
268
|
+
} finally {
|
|
269
|
+
await release();
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
async function acquireInstallLock(lockDir) {
|
|
274
|
+
const startedAt = Date.now();
|
|
275
|
+
for (;;) {
|
|
276
|
+
try {
|
|
277
|
+
await fs.promises.mkdir(lockDir);
|
|
278
|
+
return async () => {
|
|
279
|
+
await fs.promises.rmdir(lockDir).catch(() => {});
|
|
280
|
+
};
|
|
281
|
+
} catch (error) {
|
|
282
|
+
if (error.code !== "EEXIST") {
|
|
283
|
+
throw error;
|
|
284
|
+
}
|
|
285
|
+
await removeStaleLock(lockDir).catch(() => {});
|
|
286
|
+
if (Date.now() - startedAt > lockTimeoutMs) {
|
|
287
|
+
throw new Error(`install lock timed out: ${lockDir}`);
|
|
288
|
+
}
|
|
289
|
+
await delay(100);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
async function removeStaleLock(lockDir) {
|
|
295
|
+
const stat = await fs.promises.stat(lockDir);
|
|
296
|
+
if (Date.now() - stat.mtimeMs > staleLockMs) {
|
|
297
|
+
await fs.promises.rmdir(lockDir);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function delay(ms) {
|
|
302
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function writeDiagnostic(io, message) {
|
|
306
|
+
if (io.stderr && typeof io.stderr.write === "function") {
|
|
307
|
+
io.stderr.write(message);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function spawnAndWait(command, args, options = {}) {
|
|
312
|
+
return new Promise((resolve, reject) => {
|
|
313
|
+
const child = spawn(command, args, {
|
|
314
|
+
stdio: options.stdio || "inherit",
|
|
315
|
+
windowsHide: true
|
|
316
|
+
});
|
|
317
|
+
let settled = false;
|
|
318
|
+
let stdinEndHandler;
|
|
319
|
+
let stdinErrorHandler;
|
|
320
|
+
let exitHandler;
|
|
321
|
+
|
|
322
|
+
const cleanup = () => {
|
|
323
|
+
for (const [signal, handler] of signalHandlers) {
|
|
324
|
+
process.off(signal, handler);
|
|
325
|
+
}
|
|
326
|
+
if (stdinEndHandler) {
|
|
327
|
+
process.stdin.off("end", stdinEndHandler);
|
|
328
|
+
}
|
|
329
|
+
if (stdinErrorHandler) {
|
|
330
|
+
process.stdin.off("error", stdinErrorHandler);
|
|
331
|
+
}
|
|
332
|
+
if (exitHandler) {
|
|
333
|
+
process.off("exit", exitHandler);
|
|
334
|
+
}
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
const signalHandlers = ["SIGINT", "SIGTERM"].map(signal => {
|
|
338
|
+
const handler = () => {
|
|
339
|
+
if (!child.killed) {
|
|
340
|
+
child.kill(signal);
|
|
341
|
+
}
|
|
342
|
+
};
|
|
343
|
+
process.on(signal, handler);
|
|
344
|
+
return [signal, handler];
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
if (options.pipeStdin && child.stdin) {
|
|
348
|
+
child.stdin.on("error", () => {});
|
|
349
|
+
stdinEndHandler = () => child.stdin.end();
|
|
350
|
+
stdinErrorHandler = () => child.stdin.destroy();
|
|
351
|
+
process.stdin.on("end", stdinEndHandler);
|
|
352
|
+
process.stdin.on("error", stdinErrorHandler);
|
|
353
|
+
process.stdin.pipe(child.stdin);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
exitHandler = () => {
|
|
357
|
+
if (!settled && !child.killed) {
|
|
358
|
+
child.kill();
|
|
359
|
+
}
|
|
360
|
+
};
|
|
361
|
+
process.on("exit", exitHandler);
|
|
362
|
+
|
|
363
|
+
child.on("error", error => {
|
|
364
|
+
settled = true;
|
|
365
|
+
cleanup();
|
|
366
|
+
reject(error);
|
|
367
|
+
});
|
|
368
|
+
child.on("exit", (code, signal) => {
|
|
369
|
+
settled = true;
|
|
370
|
+
cleanup();
|
|
371
|
+
resolve(code == null ? signalExitCode(signal) : code);
|
|
372
|
+
});
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function signalExitCode(signal) {
|
|
377
|
+
if (signal === "SIGINT") {
|
|
378
|
+
return 130;
|
|
379
|
+
}
|
|
380
|
+
if (signal === "SIGTERM") {
|
|
381
|
+
return 143;
|
|
382
|
+
}
|
|
383
|
+
return 1;
|
|
384
|
+
}
|
|
385
|
+
|
|
101
386
|
function parseArgs(argv) {
|
|
102
387
|
const [command, ...rest] = argv;
|
|
388
|
+
if (!command) {
|
|
389
|
+
throw new Error(usage());
|
|
390
|
+
}
|
|
391
|
+
if (["install", "update", "uninstall"].includes(command)) {
|
|
392
|
+
return { command, options: parseInstallOptions(command, rest), passthroughArgs: [] };
|
|
393
|
+
}
|
|
394
|
+
if (command === "login") {
|
|
395
|
+
const launcher = parseLauncherOptions(rest, { allowPassthrough: true, rejectPrintConfig: true });
|
|
396
|
+
return { command, options: launcher.options, passthroughArgs: launcher.passthroughArgs };
|
|
397
|
+
}
|
|
398
|
+
if (command === "setup") {
|
|
399
|
+
const launcher = parseLauncherOptions(rest, {
|
|
400
|
+
allowPassthrough: true,
|
|
401
|
+
passThroughDryRun: true,
|
|
402
|
+
passThroughPrintConfig: true
|
|
403
|
+
});
|
|
404
|
+
return { command, options: launcher.options, passthroughArgs: launcher.passthroughArgs };
|
|
405
|
+
}
|
|
406
|
+
if (command === "mcp") {
|
|
407
|
+
const [subcommand, ...subcommandArgs] = rest;
|
|
408
|
+
if (!["serve", "config", "login", "status", "logout"].includes(subcommand)) {
|
|
409
|
+
throw new Error(mcpUsage());
|
|
410
|
+
}
|
|
411
|
+
const launcher = parseLauncherOptions(subcommandArgs, {
|
|
412
|
+
allowPassthrough: subcommand !== "config",
|
|
413
|
+
rejectPrintConfig: true
|
|
414
|
+
});
|
|
415
|
+
return { command, subcommand, options: launcher.options, passthroughArgs: launcher.passthroughArgs };
|
|
416
|
+
}
|
|
417
|
+
throw new Error(usage());
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function parseInstallOptions(command, rest) {
|
|
103
421
|
const options = {};
|
|
104
422
|
for (let index = 0; index < rest.length; index += 1) {
|
|
105
423
|
const arg = rest[index];
|
|
@@ -129,7 +447,58 @@ function parseArgs(argv) {
|
|
|
129
447
|
throw new Error(`unknown option ${arg}`);
|
|
130
448
|
}
|
|
131
449
|
}
|
|
132
|
-
return
|
|
450
|
+
return options;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function parseLauncherOptions(rest, settings = {}) {
|
|
454
|
+
const options = {};
|
|
455
|
+
const passthroughArgs = [];
|
|
456
|
+
for (let index = 0; index < rest.length; index += 1) {
|
|
457
|
+
const arg = rest[index];
|
|
458
|
+
switch (arg) {
|
|
459
|
+
case "--":
|
|
460
|
+
if (settings.allowPassthrough) {
|
|
461
|
+
passthroughArgs.push(...rest.slice(index + 1));
|
|
462
|
+
index = rest.length;
|
|
463
|
+
break;
|
|
464
|
+
}
|
|
465
|
+
throw new Error(`unknown option ${arg}`);
|
|
466
|
+
case "--dry-run":
|
|
467
|
+
if (settings.passThroughDryRun) {
|
|
468
|
+
passthroughArgs.push(arg);
|
|
469
|
+
break;
|
|
470
|
+
}
|
|
471
|
+
throw new Error("--dry-run is only valid for install/update/uninstall");
|
|
472
|
+
case "--print-config":
|
|
473
|
+
if (settings.passThroughPrintConfig) {
|
|
474
|
+
passthroughArgs.push(arg);
|
|
475
|
+
break;
|
|
476
|
+
}
|
|
477
|
+
if (settings.rejectPrintConfig) {
|
|
478
|
+
throw new Error("--print-config is not valid for this command");
|
|
479
|
+
}
|
|
480
|
+
options.printConfig = true;
|
|
481
|
+
break;
|
|
482
|
+
case "--install-dir":
|
|
483
|
+
options.installDir = requireValue(rest, ++index, arg);
|
|
484
|
+
break;
|
|
485
|
+
case "--from-local":
|
|
486
|
+
options.fromLocal = requireValue(rest, ++index, arg);
|
|
487
|
+
break;
|
|
488
|
+
case "--platform":
|
|
489
|
+
options.platform = requireValue(rest, ++index, arg);
|
|
490
|
+
break;
|
|
491
|
+
case "--arch":
|
|
492
|
+
options.arch = requireValue(rest, ++index, arg);
|
|
493
|
+
break;
|
|
494
|
+
default:
|
|
495
|
+
if (!settings.allowPassthrough) {
|
|
496
|
+
throw new Error(`unknown option ${arg}`);
|
|
497
|
+
}
|
|
498
|
+
passthroughArgs.push(arg);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
return { options, passthroughArgs };
|
|
133
502
|
}
|
|
134
503
|
|
|
135
504
|
function requireValue(values, index, option) {
|
|
@@ -195,6 +564,21 @@ function printMCPConfig(commandPath) {
|
|
|
195
564
|
}, null, 2));
|
|
196
565
|
}
|
|
197
566
|
|
|
567
|
+
function printUniversalMCPConfig() {
|
|
568
|
+
console.log(JSON.stringify(universalMCPConfig(), null, 2));
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
function universalMCPConfig() {
|
|
572
|
+
return {
|
|
573
|
+
mcpServers: {
|
|
574
|
+
patchxnote: {
|
|
575
|
+
command: "npx",
|
|
576
|
+
args: ["-y", "patchxnote-agent@latest", "mcp", "serve"]
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
|
|
198
582
|
function isInstallDirOnPath(installDir, pathValue, targetPlatform) {
|
|
199
583
|
if (!pathValue) {
|
|
200
584
|
return false;
|
|
@@ -284,6 +668,14 @@ function verifyChecksum(binary, assetName, checksumsText) {
|
|
|
284
668
|
}
|
|
285
669
|
}
|
|
286
670
|
|
|
671
|
+
function usage() {
|
|
672
|
+
return "usage: patchxnote-agent <install|update|uninstall|login|setup|mcp> [options]";
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
function mcpUsage() {
|
|
676
|
+
return "usage: patchxnote-agent mcp <serve|config|login|status|logout> [options]";
|
|
677
|
+
}
|
|
678
|
+
|
|
287
679
|
if (require.main === module) {
|
|
288
680
|
main(process.argv.slice(2)).catch(error => {
|
|
289
681
|
console.error(error.message);
|
|
@@ -292,12 +684,16 @@ if (require.main === module) {
|
|
|
292
684
|
}
|
|
293
685
|
|
|
294
686
|
module.exports = {
|
|
687
|
+
createInstallPlan,
|
|
295
688
|
defaultInstallDir,
|
|
689
|
+
inspectInstalledBinary,
|
|
690
|
+
parseLauncherOptions,
|
|
296
691
|
parseArgs,
|
|
297
692
|
resolveTarget,
|
|
298
693
|
joinInstallPath,
|
|
299
694
|
isInstallDirOnPath,
|
|
300
695
|
pathHint,
|
|
301
696
|
resolveRedirectURL,
|
|
697
|
+
universalMCPConfig,
|
|
302
698
|
verifyChecksum
|
|
303
699
|
};
|