patchxnote-agent 0.2.0

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 ADDED
@@ -0,0 +1,22 @@
1
+ # PatchXNote Agent npm wrapper
2
+
3
+ [English README](https://github.com/ZsTs119/patchxnote-agent#readme) | [简体中文说明](https://github.com/ZsTs119/patchxnote-agent/blob/main/README.zh-CN.md)
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.
6
+
7
+ ```sh
8
+ npx -y patchxnote-agent@0.2.0 install --print-config
9
+ ```
10
+
11
+ PatchXNote Agent runs a local stdio MCP server:
12
+
13
+ ```sh
14
+ patchxnote mcp serve
15
+ ```
16
+
17
+ Agent V1 is read-only. It stores credentials in the OS-native keychain when available, exposes safe PatchXNote account projections through dedicated `/v1/agent/**` APIs, and does not expose raw audio, full transcripts, SK, full MAC values, hardware write actions, payment flows, or Admin APIs.
18
+
19
+ For full installation, MCP setup, security notes, and troubleshooting, read the GitHub documentation:
20
+
21
+ - [English README](https://github.com/ZsTs119/patchxnote-agent#readme)
22
+ - [简体中文 README](https://github.com/ZsTs119/patchxnote-agent/blob/main/README.zh-CN.md)
@@ -0,0 +1,303 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ const crypto = require("crypto");
5
+ const fs = require("fs");
6
+ const https = require("https");
7
+ const os = require("os");
8
+ const path = require("path");
9
+
10
+ const packageRoot = path.resolve(__dirname, "..");
11
+ const packageJSON = require(path.join(packageRoot, "package.json"));
12
+ const repo = "ZsTs119/patchxnote-agent";
13
+
14
+ async function main(argv) {
15
+ 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>]");
18
+ }
19
+
20
+ const target = resolveTarget(parsed.options.platform || process.platform, parsed.options.arch || process.arch);
21
+ const installDir = parsed.options.installDir || defaultInstallDir(process.platform);
22
+ const binaryName = target.platform === "windows" ? "patchxnote.exe" : "patchxnote";
23
+ const installPath = joinInstallPath(installDir, binaryName, target.platform);
24
+ const version = packageJSON.version;
25
+ const assetName = `patchxnote_${version}_${target.platform}_${target.arch}${target.ext}`;
26
+ const releaseBaseURL = process.env.PATCHXNOTE_AGENT_RELEASE_BASE_URL
27
+ || process.env.PATCHNOTE_AGENT_RELEASE_BASE_URL
28
+ || `https://github.com/${repo}/releases/download/v${version}`;
29
+ const assetURL = `${releaseBaseURL}/${assetName}`;
30
+ const checksumsURL = `${releaseBaseURL}/checksums.txt`;
31
+
32
+ const plan = {
33
+ version,
34
+ platform: target.platform,
35
+ arch: target.arch,
36
+ install_dir: installDir,
37
+ install_path: installPath,
38
+ install_dir_on_path: isInstallDirOnPath(installDir, process.env.PATH || "", target.platform),
39
+ path_hint: pathHint(installDir, target.platform),
40
+ asset_url: assetURL,
41
+ checksums_url: checksumsURL,
42
+ action: parsed.command,
43
+ dry_run: Boolean(parsed.options.dryRun)
44
+ };
45
+
46
+ if (parsed.command === "uninstall") {
47
+ await uninstall(plan, parsed.options);
48
+ return;
49
+ }
50
+
51
+ if (parsed.options.dryRun) {
52
+ printPlan(plan);
53
+ if (parsed.options.printConfig) {
54
+ printMCPConfig(installPath);
55
+ }
56
+ return;
57
+ }
58
+
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);
66
+ }
67
+
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);
73
+ }
74
+
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");
82
+ }
83
+ }
84
+
85
+ async function uninstall(plan, options) {
86
+ if (options.dryRun) {
87
+ printPlan(plan);
88
+ return;
89
+ }
90
+ await fs.promises.rm(plan.install_path, { force: true });
91
+ console.log(`Removed PatchXNote Agent from ${plan.install_path}`);
92
+ }
93
+
94
+ async function assertExecutable(installPath) {
95
+ const stat = await fs.promises.stat(installPath);
96
+ if ((stat.mode & 0o111) === 0) {
97
+ throw new Error(`installed binary is not executable: ${installPath}`);
98
+ }
99
+ }
100
+
101
+ function parseArgs(argv) {
102
+ const [command, ...rest] = argv;
103
+ const options = {};
104
+ for (let index = 0; index < rest.length; index += 1) {
105
+ const arg = rest[index];
106
+ switch (arg) {
107
+ case "--dry-run":
108
+ options.dryRun = true;
109
+ break;
110
+ case "--print-config":
111
+ options.printConfig = true;
112
+ break;
113
+ case "--install-dir":
114
+ options.installDir = requireValue(rest, ++index, arg);
115
+ break;
116
+ case "--from-local":
117
+ if (command === "uninstall") {
118
+ throw new Error("--from-local is not valid for uninstall");
119
+ }
120
+ options.fromLocal = requireValue(rest, ++index, arg);
121
+ break;
122
+ case "--platform":
123
+ options.platform = requireValue(rest, ++index, arg);
124
+ break;
125
+ case "--arch":
126
+ options.arch = requireValue(rest, ++index, arg);
127
+ break;
128
+ default:
129
+ throw new Error(`unknown option ${arg}`);
130
+ }
131
+ }
132
+ return { command, options };
133
+ }
134
+
135
+ function requireValue(values, index, option) {
136
+ if (index >= values.length || values[index].startsWith("--")) {
137
+ throw new Error(`${option} requires a value`);
138
+ }
139
+ return values[index];
140
+ }
141
+
142
+ function resolveTarget(platform, arch) {
143
+ const platformMap = {
144
+ darwin: "darwin",
145
+ linux: "linux",
146
+ win32: "windows",
147
+ windows: "windows"
148
+ };
149
+ const archMap = {
150
+ x64: "amd64",
151
+ amd64: "amd64",
152
+ arm64: "arm64"
153
+ };
154
+ const resolvedPlatform = platformMap[platform];
155
+ const resolvedArch = archMap[arch];
156
+ if (!resolvedPlatform || !resolvedArch) {
157
+ throw new Error(`unsupported platform or architecture: ${platform}/${arch}`);
158
+ }
159
+ return {
160
+ platform: resolvedPlatform,
161
+ arch: resolvedArch,
162
+ ext: resolvedPlatform === "windows" ? ".exe" : ""
163
+ };
164
+ }
165
+
166
+ function defaultInstallDir(platform) {
167
+ if (platform === "win32") {
168
+ const base = process.env.LOCALAPPDATA || path.join(os.homedir(), "AppData", "Local");
169
+ return path.join(base, "PatchXNote Agent", "bin");
170
+ }
171
+ return path.join(os.homedir(), ".patchxnote-agent", "bin");
172
+ }
173
+
174
+ function joinInstallPath(installDir, binaryName, targetPlatform) {
175
+ if (targetPlatform === "windows") {
176
+ return path.win32.join(installDir, binaryName);
177
+ }
178
+ return path.posix.join(installDir.replace(/\\/g, "/"), binaryName);
179
+ }
180
+
181
+ function printPlan(plan) {
182
+ console.log(`PatchXNote Agent ${plan.action} dry run:`);
183
+ console.log(JSON.stringify(plan, null, 2));
184
+ }
185
+
186
+ function printMCPConfig(commandPath) {
187
+ console.log("MCP config:");
188
+ console.log(JSON.stringify({
189
+ mcpServers: {
190
+ patchxnote: {
191
+ command: commandPath,
192
+ args: ["mcp", "serve"]
193
+ }
194
+ }
195
+ }, null, 2));
196
+ }
197
+
198
+ function isInstallDirOnPath(installDir, pathValue, targetPlatform) {
199
+ if (!pathValue) {
200
+ return false;
201
+ }
202
+ const delimiter = targetPlatform === "windows" ? ";" : ":";
203
+ const normalize = targetPlatform === "windows"
204
+ ? value => path.win32.normalize(value).toLowerCase()
205
+ : value => path.posix.normalize(value.replace(/\\/g, "/"));
206
+ const expected = normalize(installDir);
207
+ return pathValue.split(delimiter).some(entry => entry.trim() && normalize(entry.trim()) === expected);
208
+ }
209
+
210
+ function pathHint(installDir, targetPlatform) {
211
+ if (targetPlatform === "windows") {
212
+ const escaped = installDir.replace(/"/g, '\\"');
213
+ return `[Environment]::SetEnvironmentVariable("Path", [Environment]::GetEnvironmentVariable("Path", "User") + ";${escaped}", "User")`;
214
+ }
215
+ return `export PATH="${installDir.replace(/"/g, '\\"')}:$PATH"`;
216
+ }
217
+
218
+ function printPathGuidance(installDir, targetPlatform) {
219
+ if (isInstallDirOnPath(installDir, process.env.PATH || "", targetPlatform)) {
220
+ return;
221
+ }
222
+ console.log("To use patchxnote directly from your terminal, add the install directory to PATH:");
223
+ console.log(pathHint(installDir, targetPlatform));
224
+ if (targetPlatform === "windows") {
225
+ console.log("Open a new terminal after updating PATH.");
226
+ }
227
+ }
228
+
229
+ function download(url, encoding, redirects = 0) {
230
+ return new Promise((resolve, reject) => {
231
+ https.get(url, response => {
232
+ if (response.statusCode >= 300 && response.statusCode < 400) {
233
+ response.resume();
234
+ if (!response.headers.location) {
235
+ reject(new Error(`download redirect missing location for ${url}`));
236
+ return;
237
+ }
238
+ if (redirects >= 5) {
239
+ reject(new Error(`download redirect limit exceeded for ${url}`));
240
+ return;
241
+ }
242
+ let nextURL;
243
+ try {
244
+ nextURL = resolveRedirectURL(url, response.headers.location);
245
+ } catch (error) {
246
+ reject(error);
247
+ return;
248
+ }
249
+ download(nextURL, encoding, redirects + 1).then(resolve, reject);
250
+ return;
251
+ }
252
+ if (response.statusCode < 200 || response.statusCode >= 300) {
253
+ response.resume();
254
+ reject(new Error(`download failed with status ${response.statusCode}`));
255
+ return;
256
+ }
257
+ const chunks = [];
258
+ response.on("data", chunk => chunks.push(chunk));
259
+ response.on("end", () => {
260
+ const body = Buffer.concat(chunks);
261
+ resolve(encoding ? body.toString(encoding) : body);
262
+ });
263
+ }).on("error", reject);
264
+ });
265
+ }
266
+
267
+ function resolveRedirectURL(currentURL, location) {
268
+ const next = new URL(location, currentURL);
269
+ if (next.protocol !== "https:") {
270
+ throw new Error(`refusing non-https redirect to ${next.protocol}`);
271
+ }
272
+ return next.toString();
273
+ }
274
+
275
+ function verifyChecksum(binary, assetName, checksumsText) {
276
+ const expectedLine = checksumsText.split(/\r?\n/).find(line => line.trim().endsWith(` ${assetName}`));
277
+ if (!expectedLine) {
278
+ throw new Error(`checksum not found for ${assetName}`);
279
+ }
280
+ const expected = expectedLine.trim().split(/\s+/)[0];
281
+ const actual = crypto.createHash("sha256").update(binary).digest("hex");
282
+ if (actual !== expected) {
283
+ throw new Error(`checksum mismatch for ${assetName}`);
284
+ }
285
+ }
286
+
287
+ if (require.main === module) {
288
+ main(process.argv.slice(2)).catch(error => {
289
+ console.error(error.message);
290
+ process.exitCode = 1;
291
+ });
292
+ }
293
+
294
+ module.exports = {
295
+ defaultInstallDir,
296
+ parseArgs,
297
+ resolveTarget,
298
+ joinInstallPath,
299
+ isInstallDirOnPath,
300
+ pathHint,
301
+ resolveRedirectURL,
302
+ verifyChecksum
303
+ };
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "patchxnote-agent",
3
+ "version": "0.2.0",
4
+ "description": "Installer wrapper for the PatchXNote Agent CLI and local MCP server.",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "https://github.com/ZsTs119/patchxnote-agent"
8
+ },
9
+ "homepage": "https://github.com/ZsTs119/patchxnote-agent#readme",
10
+ "bin": {
11
+ "patchxnote-agent": "bin/patchxnote-agent.js"
12
+ },
13
+ "files": [
14
+ "bin",
15
+ "README.md"
16
+ ],
17
+ "scripts": {
18
+ "install:local": "node ./bin/patchxnote-agent.js install",
19
+ "pack:dry-run": "npm pack --dry-run",
20
+ "test": "node ./test/install.test.js"
21
+ },
22
+ "publishConfig": {
23
+ "registry": "https://registry.npmjs.org"
24
+ },
25
+ "engines": {
26
+ "node": ">=18"
27
+ },
28
+ "license": "UNLICENSED"
29
+ }