patchxnote-agent 0.2.5 → 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 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
- 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 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
- Or run the install command manually:
15
+ Print the generic MCP config manually:
16
16
 
17
17
  ```sh
18
- npx -y patchxnote-agent@0.2.5 install --print-config
18
+ npx -y patchxnote-agent@latest mcp config
19
19
  ```
20
20
 
21
- PatchXNote Agent runs a local stdio MCP server:
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.
@@ -30,6 +42,7 @@ Common CLI examples:
30
42
 
31
43
  ```sh
32
44
  patchxnote model-io list --platform mobile
45
+ patchxnote model-io packaged-result --memory-id <memory_or_request_id> --platform mobile --out ./packaged-result.json
33
46
  patchxnote model-io provider-response --request-id <request_id> --platform mobile --out ./provider-response.json
34
47
  patchxnote webhook set "Product Feishu" --type feishu --url-stdin
35
48
  patchxnote webhook send --target "Product Feishu" --file ./message.md
@@ -37,7 +50,7 @@ patchxnote webhook send --target "Product Feishu" --file ./message.md
37
50
 
38
51
  Webhook aliases can contain Chinese text, spaces, and dots.
39
52
 
40
- Server-backed PatchXNote data access remains read-only through dedicated `/v1/agent/**` APIs. 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.
41
54
 
42
55
  For full installation, MCP setup, security notes, and troubleshooting, read the GitHub documentation:
43
56
 
@@ -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
- 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 === "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
- const target = resolveTarget(parsed.options.platform || process.platform, parsed.options.arch || process.arch);
21
- const installDir = parsed.options.installDir || defaultInstallDir(process.platform);
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: parsed.command,
43
- dry_run: Boolean(parsed.options.dryRun)
110
+ action: command,
111
+ dry_run: Boolean(options.dryRun)
44
112
  };
113
+ return plan;
114
+ }
45
115
 
46
- if (parsed.command === "uninstall") {
47
- await uninstall(plan, parsed.options);
48
- return;
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
- if (parsed.options.dryRun) {
52
- printPlan(plan);
53
- if (parsed.options.printConfig) {
54
- printMCPConfig(installPath);
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
- return;
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
- 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);
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
- 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);
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
- 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");
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 { command, options };
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
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "patchxnote-agent",
3
- "version": "0.2.5",
3
+ "version": "0.2.7",
4
4
  "description": "Installer wrapper for the PatchXNote Agent CLI and local MCP server.",
5
5
  "repository": {
6
6
  "type": "git",