patchxnote-agent 0.2.6 → 0.2.7
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 +19 -7
- package/bin/patchxnote-agent.js +390 -34
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,26 +2,38 @@
|
|
|
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 mcp config, add the printed MCP JSON config to this AI assistant, then guide me through npx -y patchxnote-agent@latest login in the editor terminal. 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
|
+
Print the generic MCP config manually:
|
|
16
16
|
|
|
17
17
|
```sh
|
|
18
|
-
npx -y patchxnote-agent@
|
|
18
|
+
npx -y patchxnote-agent@latest mcp config
|
|
19
19
|
```
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
The generated config uses this universal local stdio MCP command:
|
|
22
22
|
|
|
23
23
|
```sh
|
|
24
|
-
patchxnote mcp serve
|
|
24
|
+
npx -y patchxnote-agent@latest mcp serve
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Log in from the editor terminal:
|
|
28
|
+
|
|
29
|
+
```sh
|
|
30
|
+
npx -y patchxnote-agent@latest login
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
If a client rejects `npx` or times out during first binary download, run the stable absolute-path fallback:
|
|
34
|
+
|
|
35
|
+
```sh
|
|
36
|
+
npx -y patchxnote-agent@latest install --print-config
|
|
25
37
|
```
|
|
26
38
|
|
|
27
39
|
It currently exposes 19 MCP tools grouped around account/record lookup, webhook configuration and sending, and explicit AI result inspection.
|
|
@@ -38,7 +50,7 @@ patchxnote webhook send --target "Product Feishu" --file ./message.md
|
|
|
38
50
|
|
|
39
51
|
Webhook aliases can contain Chinese text, spaces, and dots.
|
|
40
52
|
|
|
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.
|
|
53
|
+
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
54
|
|
|
43
55
|
For full installation, MCP setup, security notes, and troubleshooting, read the GitHub documentation:
|
|
44
56
|
|
package/bin/patchxnote-agent.js
CHANGED
|
@@ -6,19 +6,86 @@ 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 === "mcp" && parsed.subcommand === "config") {
|
|
33
|
+
printUniversalMCPConfig();
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
if (parsed.command === "mcp" && parsed.subcommand === "serve") {
|
|
37
|
+
await runMCPServe(parsed);
|
|
38
|
+
return;
|
|
18
39
|
}
|
|
19
40
|
|
|
20
|
-
|
|
21
|
-
|
|
41
|
+
throw new Error(usage());
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function runInstall(parsed) {
|
|
45
|
+
const plan = createInstallPlan(parsed.command, parsed.options);
|
|
46
|
+
if (parsed.options.dryRun) {
|
|
47
|
+
printPlan(plan);
|
|
48
|
+
if (parsed.options.printConfig) {
|
|
49
|
+
printMCPConfig(plan.install_path);
|
|
50
|
+
}
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
await withInstallLock(plan, async () => installBinary(plan, parsed.options));
|
|
54
|
+
console.log(`Installed PatchXNote Agent ${plan.version} to ${plan.install_path}`);
|
|
55
|
+
printPathGuidance(plan.install_dir, plan.platform);
|
|
56
|
+
if (parsed.options.printConfig) {
|
|
57
|
+
printMCPConfig(plan.install_path);
|
|
58
|
+
} else {
|
|
59
|
+
console.log("Run: patchxnote login");
|
|
60
|
+
console.log("MCP config: patchxnote mcp serve");
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function runUninstall(parsed) {
|
|
65
|
+
const plan = createInstallPlan(parsed.command, parsed.options);
|
|
66
|
+
await uninstall(plan, parsed.options);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function runLogin(parsed) {
|
|
70
|
+
const plan = await ensureBinary(parsed.options, { stderr: process.stderr });
|
|
71
|
+
const code = await spawnAndWait(plan.install_path, ["login", ...parsed.passthroughArgs], {
|
|
72
|
+
stdio: "inherit"
|
|
73
|
+
});
|
|
74
|
+
process.exitCode = code;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function runMCPServe(parsed) {
|
|
78
|
+
const plan = await ensureBinary(parsed.options, { stderr: process.stderr });
|
|
79
|
+
const code = await spawnAndWait(plan.install_path, ["mcp", "serve", ...parsed.passthroughArgs], {
|
|
80
|
+
stdio: ["pipe", "inherit", "inherit"],
|
|
81
|
+
pipeStdin: true
|
|
82
|
+
});
|
|
83
|
+
process.exitCode = code;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function createInstallPlan(command, options = {}) {
|
|
87
|
+
const target = resolveTarget(options.platform || process.platform, options.arch || process.arch);
|
|
88
|
+
const installDir = options.installDir || defaultInstallDir(process.platform);
|
|
22
89
|
const binaryName = target.platform === "windows" ? "patchxnote.exe" : "patchxnote";
|
|
23
90
|
const installPath = joinInstallPath(installDir, binaryName, target.platform);
|
|
24
91
|
const version = packageJSON.version;
|
|
@@ -37,48 +104,65 @@ async function main(argv) {
|
|
|
37
104
|
install_path: installPath,
|
|
38
105
|
install_dir_on_path: isInstallDirOnPath(installDir, process.env.PATH || "", target.platform),
|
|
39
106
|
path_hint: pathHint(installDir, target.platform),
|
|
107
|
+
asset_name: assetName,
|
|
40
108
|
asset_url: assetURL,
|
|
41
109
|
checksums_url: checksumsURL,
|
|
42
|
-
action:
|
|
43
|
-
dry_run: Boolean(
|
|
110
|
+
action: command,
|
|
111
|
+
dry_run: Boolean(options.dryRun)
|
|
44
112
|
};
|
|
113
|
+
return plan;
|
|
114
|
+
}
|
|
45
115
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
116
|
+
async function installBinary(plan, options = {}) {
|
|
117
|
+
let binary;
|
|
118
|
+
if (options.fromLocal) {
|
|
119
|
+
binary = await fs.promises.readFile(options.fromLocal);
|
|
120
|
+
} else {
|
|
121
|
+
binary = await download(plan.asset_url);
|
|
122
|
+
const checksums = await download(plan.checksums_url, "utf8");
|
|
123
|
+
verifyChecksum(binary, plan.asset_name, checksums);
|
|
49
124
|
}
|
|
50
125
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
126
|
+
await fs.promises.mkdir(plan.install_dir, { recursive: true, mode: 0o755 });
|
|
127
|
+
const tempPath = `${plan.install_path}.tmp-${process.pid}-${Date.now()}`;
|
|
128
|
+
try {
|
|
129
|
+
await fs.promises.writeFile(tempPath, binary, { mode: plan.platform === "windows" ? 0o644 : 0o755 });
|
|
130
|
+
if (plan.platform !== "windows") {
|
|
131
|
+
await fs.promises.chmod(tempPath, 0o755);
|
|
132
|
+
await assertExecutable(tempPath);
|
|
55
133
|
}
|
|
56
|
-
|
|
134
|
+
await replaceInstalledBinary(tempPath, plan.install_path);
|
|
135
|
+
if (plan.platform !== "windows") {
|
|
136
|
+
await assertExecutable(plan.install_path);
|
|
137
|
+
}
|
|
138
|
+
} finally {
|
|
139
|
+
await fs.promises.rm(tempPath, { force: true }).catch(() => {});
|
|
57
140
|
}
|
|
141
|
+
}
|
|
58
142
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
143
|
+
async function replaceInstalledBinary(tempPath, installPath) {
|
|
144
|
+
const backupPath = `${installPath}.bak-${process.pid}-${Date.now()}`;
|
|
145
|
+
let hasBackup = false;
|
|
146
|
+
try {
|
|
147
|
+
await fs.promises.rename(installPath, backupPath);
|
|
148
|
+
hasBackup = true;
|
|
149
|
+
} catch (error) {
|
|
150
|
+
if (error.code !== "ENOENT") {
|
|
151
|
+
throw error;
|
|
152
|
+
}
|
|
66
153
|
}
|
|
67
154
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
155
|
+
try {
|
|
156
|
+
await fs.promises.rename(tempPath, installPath);
|
|
157
|
+
} catch (error) {
|
|
158
|
+
if (hasBackup) {
|
|
159
|
+
await fs.promises.rename(backupPath, installPath).catch(() => {});
|
|
160
|
+
}
|
|
161
|
+
throw error;
|
|
73
162
|
}
|
|
74
163
|
|
|
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");
|
|
164
|
+
if (hasBackup) {
|
|
165
|
+
await fs.promises.rm(backupPath, { force: true }).catch(() => {});
|
|
82
166
|
}
|
|
83
167
|
}
|
|
84
168
|
|
|
@@ -98,8 +182,210 @@ async function assertExecutable(installPath) {
|
|
|
98
182
|
}
|
|
99
183
|
}
|
|
100
184
|
|
|
185
|
+
async function ensureBinary(options = {}, io = {}) {
|
|
186
|
+
const plan = createInstallPlan("install", options);
|
|
187
|
+
return withInstallLock(plan, async () => {
|
|
188
|
+
const inspected = inspectInstalledBinary(plan);
|
|
189
|
+
if (inspected.ok && inspected.version === plan.version) {
|
|
190
|
+
return plan;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (!inspected.exists) {
|
|
194
|
+
writeDiagnostic(io, `PatchXNote Agent binary missing; installing ${plan.version}.\n`);
|
|
195
|
+
} else if (inspected.ok) {
|
|
196
|
+
writeDiagnostic(io, `PatchXNote Agent binary version ${inspected.version} does not match package ${plan.version}; reinstalling.\n`);
|
|
197
|
+
} else {
|
|
198
|
+
writeDiagnostic(io, `PatchXNote Agent binary preflight failed; reinstalling ${plan.version}.\n`);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
await installBinary(plan, options);
|
|
202
|
+
const afterInstall = inspectInstalledBinary(plan);
|
|
203
|
+
if (!afterInstall.ok) {
|
|
204
|
+
throw new Error(`installed PatchXNote Agent binary failed preflight: ${afterInstall.reason}`);
|
|
205
|
+
}
|
|
206
|
+
if (afterInstall.version !== plan.version) {
|
|
207
|
+
throw new Error(`installed PatchXNote Agent binary version ${afterInstall.version} does not match package ${plan.version}`);
|
|
208
|
+
}
|
|
209
|
+
return plan;
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function inspectInstalledBinary(plan) {
|
|
214
|
+
if (!fs.existsSync(plan.install_path)) {
|
|
215
|
+
return { exists: false, ok: false, reason: "missing" };
|
|
216
|
+
}
|
|
217
|
+
const result = spawnSync(plan.install_path, ["version", "--output", "json"], {
|
|
218
|
+
encoding: "utf8",
|
|
219
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
220
|
+
timeout: 5000,
|
|
221
|
+
windowsHide: true,
|
|
222
|
+
maxBuffer: 1024 * 1024
|
|
223
|
+
});
|
|
224
|
+
if (result.error) {
|
|
225
|
+
return { exists: true, ok: false, reason: result.error.message };
|
|
226
|
+
}
|
|
227
|
+
if (result.status !== 0) {
|
|
228
|
+
return { exists: true, ok: false, reason: `exit ${result.status}` };
|
|
229
|
+
}
|
|
230
|
+
let decoded;
|
|
231
|
+
try {
|
|
232
|
+
decoded = JSON.parse(result.stdout.trim());
|
|
233
|
+
} catch (error) {
|
|
234
|
+
return { exists: true, ok: false, reason: "invalid version output" };
|
|
235
|
+
}
|
|
236
|
+
return { exists: true, ok: true, version: String(decoded.version || "") };
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
async function withInstallLock(plan, action) {
|
|
240
|
+
await fs.promises.mkdir(plan.install_dir, { recursive: true, mode: 0o755 });
|
|
241
|
+
const release = await acquireInstallLock(`${plan.install_path}.lock`);
|
|
242
|
+
try {
|
|
243
|
+
return await action();
|
|
244
|
+
} finally {
|
|
245
|
+
await release();
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
async function acquireInstallLock(lockDir) {
|
|
250
|
+
const startedAt = Date.now();
|
|
251
|
+
for (;;) {
|
|
252
|
+
try {
|
|
253
|
+
await fs.promises.mkdir(lockDir);
|
|
254
|
+
return async () => {
|
|
255
|
+
await fs.promises.rmdir(lockDir).catch(() => {});
|
|
256
|
+
};
|
|
257
|
+
} catch (error) {
|
|
258
|
+
if (error.code !== "EEXIST") {
|
|
259
|
+
throw error;
|
|
260
|
+
}
|
|
261
|
+
await removeStaleLock(lockDir).catch(() => {});
|
|
262
|
+
if (Date.now() - startedAt > lockTimeoutMs) {
|
|
263
|
+
throw new Error(`install lock timed out: ${lockDir}`);
|
|
264
|
+
}
|
|
265
|
+
await delay(100);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
async function removeStaleLock(lockDir) {
|
|
271
|
+
const stat = await fs.promises.stat(lockDir);
|
|
272
|
+
if (Date.now() - stat.mtimeMs > staleLockMs) {
|
|
273
|
+
await fs.promises.rmdir(lockDir);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function delay(ms) {
|
|
278
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function writeDiagnostic(io, message) {
|
|
282
|
+
if (io.stderr && typeof io.stderr.write === "function") {
|
|
283
|
+
io.stderr.write(message);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function spawnAndWait(command, args, options = {}) {
|
|
288
|
+
return new Promise((resolve, reject) => {
|
|
289
|
+
const child = spawn(command, args, {
|
|
290
|
+
stdio: options.stdio || "inherit",
|
|
291
|
+
windowsHide: true
|
|
292
|
+
});
|
|
293
|
+
let settled = false;
|
|
294
|
+
let stdinEndHandler;
|
|
295
|
+
let stdinErrorHandler;
|
|
296
|
+
let exitHandler;
|
|
297
|
+
|
|
298
|
+
const cleanup = () => {
|
|
299
|
+
for (const [signal, handler] of signalHandlers) {
|
|
300
|
+
process.off(signal, handler);
|
|
301
|
+
}
|
|
302
|
+
if (stdinEndHandler) {
|
|
303
|
+
process.stdin.off("end", stdinEndHandler);
|
|
304
|
+
}
|
|
305
|
+
if (stdinErrorHandler) {
|
|
306
|
+
process.stdin.off("error", stdinErrorHandler);
|
|
307
|
+
}
|
|
308
|
+
if (exitHandler) {
|
|
309
|
+
process.off("exit", exitHandler);
|
|
310
|
+
}
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
const signalHandlers = ["SIGINT", "SIGTERM"].map(signal => {
|
|
314
|
+
const handler = () => {
|
|
315
|
+
if (!child.killed) {
|
|
316
|
+
child.kill(signal);
|
|
317
|
+
}
|
|
318
|
+
};
|
|
319
|
+
process.on(signal, handler);
|
|
320
|
+
return [signal, handler];
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
if (options.pipeStdin && child.stdin) {
|
|
324
|
+
child.stdin.on("error", () => {});
|
|
325
|
+
stdinEndHandler = () => child.stdin.end();
|
|
326
|
+
stdinErrorHandler = () => child.stdin.destroy();
|
|
327
|
+
process.stdin.on("end", stdinEndHandler);
|
|
328
|
+
process.stdin.on("error", stdinErrorHandler);
|
|
329
|
+
process.stdin.pipe(child.stdin);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
exitHandler = () => {
|
|
333
|
+
if (!settled && !child.killed) {
|
|
334
|
+
child.kill();
|
|
335
|
+
}
|
|
336
|
+
};
|
|
337
|
+
process.on("exit", exitHandler);
|
|
338
|
+
|
|
339
|
+
child.on("error", error => {
|
|
340
|
+
settled = true;
|
|
341
|
+
cleanup();
|
|
342
|
+
reject(error);
|
|
343
|
+
});
|
|
344
|
+
child.on("exit", (code, signal) => {
|
|
345
|
+
settled = true;
|
|
346
|
+
cleanup();
|
|
347
|
+
resolve(code == null ? signalExitCode(signal) : code);
|
|
348
|
+
});
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function signalExitCode(signal) {
|
|
353
|
+
if (signal === "SIGINT") {
|
|
354
|
+
return 130;
|
|
355
|
+
}
|
|
356
|
+
if (signal === "SIGTERM") {
|
|
357
|
+
return 143;
|
|
358
|
+
}
|
|
359
|
+
return 1;
|
|
360
|
+
}
|
|
361
|
+
|
|
101
362
|
function parseArgs(argv) {
|
|
102
363
|
const [command, ...rest] = argv;
|
|
364
|
+
if (!command) {
|
|
365
|
+
throw new Error(usage());
|
|
366
|
+
}
|
|
367
|
+
if (["install", "update", "uninstall"].includes(command)) {
|
|
368
|
+
return { command, options: parseInstallOptions(command, rest), passthroughArgs: [] };
|
|
369
|
+
}
|
|
370
|
+
if (command === "login") {
|
|
371
|
+
const launcher = parseLauncherOptions(rest, { allowPassthrough: true, rejectPrintConfig: true });
|
|
372
|
+
return { command, options: launcher.options, passthroughArgs: launcher.passthroughArgs };
|
|
373
|
+
}
|
|
374
|
+
if (command === "mcp") {
|
|
375
|
+
const [subcommand, ...subcommandArgs] = rest;
|
|
376
|
+
if (!["serve", "config"].includes(subcommand)) {
|
|
377
|
+
throw new Error(mcpUsage());
|
|
378
|
+
}
|
|
379
|
+
const launcher = parseLauncherOptions(subcommandArgs, {
|
|
380
|
+
allowPassthrough: subcommand === "serve",
|
|
381
|
+
rejectPrintConfig: true
|
|
382
|
+
});
|
|
383
|
+
return { command, subcommand, options: launcher.options, passthroughArgs: launcher.passthroughArgs };
|
|
384
|
+
}
|
|
385
|
+
throw new Error(usage());
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function parseInstallOptions(command, rest) {
|
|
103
389
|
const options = {};
|
|
104
390
|
for (let index = 0; index < rest.length; index += 1) {
|
|
105
391
|
const arg = rest[index];
|
|
@@ -129,7 +415,50 @@ function parseArgs(argv) {
|
|
|
129
415
|
throw new Error(`unknown option ${arg}`);
|
|
130
416
|
}
|
|
131
417
|
}
|
|
132
|
-
return
|
|
418
|
+
return options;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function parseLauncherOptions(rest, settings = {}) {
|
|
422
|
+
const options = {};
|
|
423
|
+
const passthroughArgs = [];
|
|
424
|
+
for (let index = 0; index < rest.length; index += 1) {
|
|
425
|
+
const arg = rest[index];
|
|
426
|
+
switch (arg) {
|
|
427
|
+
case "--":
|
|
428
|
+
if (settings.allowPassthrough) {
|
|
429
|
+
passthroughArgs.push(...rest.slice(index + 1));
|
|
430
|
+
index = rest.length;
|
|
431
|
+
break;
|
|
432
|
+
}
|
|
433
|
+
throw new Error(`unknown option ${arg}`);
|
|
434
|
+
case "--dry-run":
|
|
435
|
+
throw new Error("--dry-run is only valid for install/update/uninstall");
|
|
436
|
+
case "--print-config":
|
|
437
|
+
if (settings.rejectPrintConfig) {
|
|
438
|
+
throw new Error("--print-config is not valid for this command");
|
|
439
|
+
}
|
|
440
|
+
options.printConfig = true;
|
|
441
|
+
break;
|
|
442
|
+
case "--install-dir":
|
|
443
|
+
options.installDir = requireValue(rest, ++index, arg);
|
|
444
|
+
break;
|
|
445
|
+
case "--from-local":
|
|
446
|
+
options.fromLocal = requireValue(rest, ++index, arg);
|
|
447
|
+
break;
|
|
448
|
+
case "--platform":
|
|
449
|
+
options.platform = requireValue(rest, ++index, arg);
|
|
450
|
+
break;
|
|
451
|
+
case "--arch":
|
|
452
|
+
options.arch = requireValue(rest, ++index, arg);
|
|
453
|
+
break;
|
|
454
|
+
default:
|
|
455
|
+
if (!settings.allowPassthrough) {
|
|
456
|
+
throw new Error(`unknown option ${arg}`);
|
|
457
|
+
}
|
|
458
|
+
passthroughArgs.push(arg);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
return { options, passthroughArgs };
|
|
133
462
|
}
|
|
134
463
|
|
|
135
464
|
function requireValue(values, index, option) {
|
|
@@ -195,6 +524,21 @@ function printMCPConfig(commandPath) {
|
|
|
195
524
|
}, null, 2));
|
|
196
525
|
}
|
|
197
526
|
|
|
527
|
+
function printUniversalMCPConfig() {
|
|
528
|
+
console.log(JSON.stringify(universalMCPConfig(), null, 2));
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
function universalMCPConfig() {
|
|
532
|
+
return {
|
|
533
|
+
mcpServers: {
|
|
534
|
+
patchxnote: {
|
|
535
|
+
command: "npx",
|
|
536
|
+
args: ["-y", "patchxnote-agent@latest", "mcp", "serve"]
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
|
|
198
542
|
function isInstallDirOnPath(installDir, pathValue, targetPlatform) {
|
|
199
543
|
if (!pathValue) {
|
|
200
544
|
return false;
|
|
@@ -284,6 +628,14 @@ function verifyChecksum(binary, assetName, checksumsText) {
|
|
|
284
628
|
}
|
|
285
629
|
}
|
|
286
630
|
|
|
631
|
+
function usage() {
|
|
632
|
+
return "usage: patchxnote-agent <install|update|uninstall|login|mcp> [options]";
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
function mcpUsage() {
|
|
636
|
+
return "usage: patchxnote-agent mcp <serve|config> [options]";
|
|
637
|
+
}
|
|
638
|
+
|
|
287
639
|
if (require.main === module) {
|
|
288
640
|
main(process.argv.slice(2)).catch(error => {
|
|
289
641
|
console.error(error.message);
|
|
@@ -292,12 +644,16 @@ if (require.main === module) {
|
|
|
292
644
|
}
|
|
293
645
|
|
|
294
646
|
module.exports = {
|
|
647
|
+
createInstallPlan,
|
|
295
648
|
defaultInstallDir,
|
|
649
|
+
inspectInstalledBinary,
|
|
650
|
+
parseLauncherOptions,
|
|
296
651
|
parseArgs,
|
|
297
652
|
resolveTarget,
|
|
298
653
|
joinInstallPath,
|
|
299
654
|
isInstallDirOnPath,
|
|
300
655
|
pathHint,
|
|
301
656
|
resolveRedirectURL,
|
|
657
|
+
universalMCPConfig,
|
|
302
658
|
verifyChecksum
|
|
303
659
|
};
|