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 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
- Install and connect PatchXNote Agent by following the public guide at https://patchx2025.feishu.cn/wiki/PnVRwYT7IirFPckairGcWPnHnCd: run npx -y patchxnote-agent install --print-config, guide me through patchxnote login, and add the printed MCP JSON config to this AI assistant. Do not ask me to paste OTP codes, access tokens, or refresh tokens into chat. GitHub repository: https://github.com/ZsTs119/patchxnote-agent
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
- Or run the install command manually:
15
+ Run setup for a supported local client:
16
16
 
17
17
  ```sh
18
- npx -y patchxnote-agent@0.2.6 install --print-config
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
- PatchXNote Agent runs a local stdio MCP server:
23
+ Log in with browser OAuth, then check or clear the MCP login state:
22
24
 
23
25
  ```sh
24
- patchxnote mcp serve
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
 
@@ -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
- if (!["install", "update", "uninstall"].includes(parsed.command)) {
17
- throw new Error("usage: patchxnote-agent <install|update|uninstall> [--dry-run] [--print-config] [--install-dir <path>]");
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
- const target = resolveTarget(parsed.options.platform || process.platform, parsed.options.arch || process.arch);
21
- const installDir = parsed.options.installDir || defaultInstallDir(process.platform);
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: parsed.command,
43
- dry_run: Boolean(parsed.options.dryRun)
134
+ action: command,
135
+ dry_run: Boolean(options.dryRun)
44
136
  };
137
+ return plan;
138
+ }
45
139
 
46
- if (parsed.command === "uninstall") {
47
- await uninstall(plan, parsed.options);
48
- return;
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
- if (parsed.options.dryRun) {
52
- printPlan(plan);
53
- if (parsed.options.printConfig) {
54
- printMCPConfig(installPath);
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
- return;
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
- let binary;
60
- if (parsed.options.fromLocal) {
61
- binary = await fs.promises.readFile(parsed.options.fromLocal);
62
- } else {
63
- binary = await download(assetURL);
64
- const checksums = await download(checksumsURL, "utf8");
65
- verifyChecksum(binary, assetName, checksums);
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
- await fs.promises.mkdir(installDir, { recursive: true, mode: 0o755 });
69
- await fs.promises.writeFile(installPath, binary, { mode: target.platform === "windows" ? 0o644 : 0o755 });
70
- if (target.platform !== "windows") {
71
- await fs.promises.chmod(installPath, 0o755);
72
- await assertExecutable(installPath);
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
- console.log(`Installed PatchXNote Agent ${version} to ${installPath}`);
76
- printPathGuidance(installDir, target.platform);
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 { command, options };
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
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "patchxnote-agent",
3
- "version": "0.2.6",
3
+ "version": "0.2.8",
4
4
  "description": "Installer wrapper for the PatchXNote Agent CLI and local MCP server.",
5
5
  "repository": {
6
6
  "type": "git",