error-mom 0.6.0 → 0.7.1

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.
Files changed (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +92 -2
  3. package/dist/cli.js +75 -38
  4. package/package.json +8 -8
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ken Kai
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,14 +1,104 @@
1
1
  # error-mom
2
2
 
3
- Agent-first CLI and MCP tools for a self-hosted Error Mom incident desk.
3
+ Agent-first CLI and MCP tools for a self-hosted [Error Mom](https://github.com/KenKaiii/error-mom) deployment.
4
+
5
+ ## Install and authenticate
4
6
 
5
7
  ```bash
6
8
  npm install --global error-mom
7
9
  error-mom login https://errors.example.com --token "$ERROR_MOM_ADMIN_TOKEN"
10
+ ```
11
+
12
+ Login stores the server URL and private admin token in `~/.error-mom/config.json` with private file permissions. You can instead set `ERROR_MOM_SERVER` and `ERROR_MOM_ADMIN_TOKEN`.
13
+
14
+ ## Connect an app
15
+
16
+ Run this from the app you want to monitor:
17
+
18
+ ```bash
19
+ error-mom init
20
+ ```
21
+
22
+ `init` creates or reuses a project, installs `@kenkaiiii/error-mom`, generates a setup file, detects the framework, and returns exact wiring instructions. For a pnpm or Yarn workspace, use `error-mom init --skip-install` and install the SDK with the workspace package manager.
23
+
24
+ The generated project key is write-only and safe to commit or ship. It can submit events but cannot read issues, manage projects, or upload source maps. Keep the admin token private.
25
+
26
+ Verify health and ingestion with the exact `error-mom doctor --project-key ...` command printed by `init`. Synthetic doctor events are validated but never stored as issues.
27
+
28
+ ## Work with projects and issues
29
+
30
+ ```bash
8
31
  error-mom projects
9
32
  error-mom issues
33
+ error-mom issues --project <project-id>
10
34
  error-mom inspect <issue-id>
35
+ error-mom inspect <issue-id> --samples 5
11
36
  error-mom resolve <issue-id> --release 1.4.2
12
37
  ```
13
38
 
14
- Run `error-mom mcp` as a stdio MCP server. It exposes `list_projects`, `list_issues`, `get_issue`, and `resolve_issue`.
39
+ Resolved issues stay hidden by default. If the same issue returns in the fixed release or a newer release, Error Mom marks it as regressed.
40
+
41
+ Permanently deleting a project also deletes its issues, samples, ingest keys, and receipts:
42
+
43
+ ```bash
44
+ error-mom delete-project <project-id>
45
+ ```
46
+
47
+ ## Source maps
48
+
49
+ Upload source maps immediately after a production build. The release must exactly match the release reported by the SDK.
50
+
51
+ ```bash
52
+ error-mom sourcemaps <build-directory> \
53
+ --release <app-version> \
54
+ --project <project-slug>
55
+ ```
56
+
57
+ The command discovers JavaScript/source-map pairs and warns about missing `sourcesContent` or release mismatches. Uploads require the admin token; project ingest keys are rejected.
58
+
59
+ Test symbolication without storing an event or map:
60
+
61
+ ```bash
62
+ error-mom doctor --symbolication
63
+ ```
64
+
65
+ ## MCP server
66
+
67
+ Run `error-mom mcp` as a stdio MCP server. It exposes:
68
+
69
+ - `list_projects`
70
+ - `list_issues`
71
+ - `get_issue`
72
+ - `resolve_issue`
73
+ - `check_symbolication`
74
+ - `delete_project`
75
+
76
+ Example client configuration:
77
+
78
+ ```json
79
+ {
80
+ "mcpServers": {
81
+ "error-mom": {
82
+ "command": "error-mom",
83
+ "args": ["mcp"],
84
+ "env": {
85
+ "ERROR_MOM_SERVER": "https://errors.example.com",
86
+ "ERROR_MOM_ADMIN_TOKEN": "set-this-in-your-secret-store"
87
+ }
88
+ }
89
+ }
90
+ }
91
+ ```
92
+
93
+ Prefer your MCP client's secure environment or secret configuration instead of committing the admin token.
94
+
95
+ ## Command help
96
+
97
+ ```bash
98
+ error-mom --help
99
+ error-mom <command> --help
100
+ ```
101
+
102
+ Operational commands write machine-readable JSON to stdout, making the CLI suitable for coding agents and automation.
103
+
104
+ See the [repository README](https://github.com/KenKaiii/error-mom#readme) for deployment, SDK, framework, dashboard, and security instructions.
package/dist/cli.js CHANGED
@@ -2,17 +2,73 @@
2
2
 
3
3
  // src/cli.ts
4
4
  import { execFileSync } from "child_process";
5
- import { chmod, mkdir, readFile, readdir, stat, writeFile } from "fs/promises";
5
+ import { chmod, mkdir, readFile as readFile2, stat, writeFile } from "fs/promises";
6
6
  import { existsSync } from "fs";
7
7
  import { homedir } from "os";
8
- import { basename, join } from "path";
8
+ import { basename as basename2, join as join2 } from "path";
9
9
  import { Command } from "commander";
10
10
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
11
11
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
12
12
  import { z } from "zod";
13
- var VERSION = "0.6.0";
14
- var CONFIG_DIR = join(homedir(), ".error-mom");
15
- var CONFIG_FILE = join(CONFIG_DIR, "config.json");
13
+
14
+ // src/sourcemap-files.ts
15
+ import { readFile, readdir } from "fs/promises";
16
+ import { basename, dirname, join } from "path";
17
+ async function associateMaps(dir) {
18
+ const { jsFiles, mapFiles } = await findBuildFiles(dir);
19
+ const associations = /* @__PURE__ */ new Map();
20
+ const referencedMaps = /* @__PURE__ */ new Set();
21
+ for (const jsFile of jsFiles) {
22
+ const contents = await readFile(jsFile, "utf8");
23
+ const matches = [...contents.matchAll(/\/\/[#@] sourceMappingURL=([^\s'"]+)/g)];
24
+ const reference = matches[matches.length - 1]?.[1];
25
+ if (!reference || reference.startsWith("data:")) continue;
26
+ const mapFile = join(dirname(jsFile), decodeURIComponent(reference));
27
+ if (!mapFiles.includes(mapFile)) continue;
28
+ const fileName = basename(jsFile);
29
+ referencedMaps.add(mapFile);
30
+ associations.set(`${mapFile}\0${fileName}`, { mapFile, fileName });
31
+ }
32
+ for (const mapFile of mapFiles) {
33
+ if (referencedMaps.has(mapFile)) continue;
34
+ associations.set(`${mapFile}\0`, {
35
+ mapFile,
36
+ fileName: basename(mapFile).replace(/\.map$/, "")
37
+ });
38
+ }
39
+ return [...associations.values()].sort((a, b) => a.fileName.localeCompare(b.fileName));
40
+ }
41
+ async function findBuildFiles(dir) {
42
+ const jsFiles = [];
43
+ const mapFiles = [];
44
+ let entries;
45
+ try {
46
+ entries = await readdir(dir, { withFileTypes: true });
47
+ } catch {
48
+ throw new Error(`Cannot read directory ${dir}.`);
49
+ }
50
+ for (const entry of entries) {
51
+ const fullPath = join(dir, entry.name);
52
+ if (entry.isDirectory() && entry.name !== "node_modules") {
53
+ const nested = await findBuildFiles(fullPath);
54
+ jsFiles.push(...nested.jsFiles);
55
+ mapFiles.push(...nested.mapFiles);
56
+ } else if (entry.isFile() && entry.name.endsWith(".map")) {
57
+ mapFiles.push(fullPath);
58
+ } else if (entry.isFile() && /\.(js|mjs|cjs)$/.test(entry.name)) {
59
+ jsFiles.push(fullPath);
60
+ }
61
+ }
62
+ return { jsFiles: jsFiles.sort(), mapFiles: mapFiles.sort() };
63
+ }
64
+
65
+ // src/cli.ts
66
+ var VERSION = "0.7.1";
67
+ var CONFIG_DIR = join2(homedir(), ".error-mom");
68
+ var CONFIG_FILE = join2(CONFIG_DIR, "config.json");
69
+ function slugifyName(value) {
70
+ return value.toLowerCase().normalize("NFKD").replace(/[\u0300-\u036f]/g, "").replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 64);
71
+ }
16
72
  var program = new Command().name("error-mom").description("Query and operate a self-hosted Error Mom incident desk").version(VERSION);
17
73
  program.command("login").description("Save the URL and private admin token for one Error Mom deployment").argument("<server>", "Error Mom server URL").requiredOption("--token <token>", "ERROR_MOM_ADMIN_TOKEN value").action(async (server, options) => {
18
74
  const normalized = normalizeServer(server);
@@ -120,14 +176,14 @@ program.command("doctor").description("Verify collector health, credentials, and
120
176
  program.command("init").description("Create/select a project, install the SDK, and generate framework-aware setup").option("--name <name>", "Project name").option("--project <slug>", "Use an existing project slug").option("--skip-install", "Generate setup without invoking the package manager").action(async (options) => {
121
177
  const config = await loadConfig();
122
178
  const packageJson = await readPackageJson(process.cwd());
123
- const name = options.name ?? (typeof packageJson.name === "string" ? packageJson.name : basename(process.cwd()));
179
+ const name = options.name ?? (typeof packageJson.name === "string" ? packageJson.name : basename2(process.cwd()));
124
180
  const projectResponse = await request(
125
181
  config.server,
126
182
  config.adminToken,
127
183
  "/api/v1/projects"
128
184
  );
129
185
  let project = options.project ? projectResponse.projects.find((candidate) => candidate.slug === options.project) : projectResponse.projects.find(
130
- (candidate) => candidate.name.toLowerCase() === name.toLowerCase()
186
+ (candidate) => candidate.name.toLowerCase() === name.toLowerCase() || candidate.slug === slugifyName(name)
131
187
  );
132
188
  if (!project) {
133
189
  const created = await request(config.server, config.adminToken, "/api/v1/projects", {
@@ -160,14 +216,14 @@ program.command("init").description("Create/select a project, install the SDK, a
160
216
  });
161
217
  program.command("sourcemaps").description("Upload production source maps so minified stacks symbolicate on ingest").argument("<dir>", "Build output directory containing *.map files (e.g. dist)").requiredOption("--release <release>", "Release the maps belong to (must match the SDK release)").requiredOption("--project <id-or-slug>", "Project id or slug").action(async (dir, options) => {
162
218
  const config = await loadConfig();
163
- const mapFiles = await findMapFiles(dir);
164
- if (mapFiles.length === 0) {
219
+ const associations = await associateMaps(dir);
220
+ if (associations.length === 0) {
165
221
  throw new Error(`No .map files found under ${dir}. Build with source maps enabled first.`);
166
222
  }
167
223
  const uploaded = [];
168
224
  const skipped = [];
169
225
  const warnings = [];
170
- for (const mapFile of mapFiles) {
226
+ for (const { mapFile, fileName } of associations) {
171
227
  const info = await stat(mapFile);
172
228
  if (info.size > 20 * 1024 * 1024) {
173
229
  skipped.push({ file: mapFile, reason: "larger than 20 MB" });
@@ -175,12 +231,11 @@ program.command("sourcemaps").description("Upload production source maps so mini
175
231
  }
176
232
  let map;
177
233
  try {
178
- map = JSON.parse(await readFile(mapFile, "utf8"));
234
+ map = JSON.parse(await readFile2(mapFile, "utf8"));
179
235
  } catch {
180
236
  skipped.push({ file: mapFile, reason: "not valid JSON" });
181
237
  continue;
182
238
  }
183
- const fileName = basename(mapFile).replace(/\.map$/, "");
184
239
  const sourcesContent = map.sourcesContent;
185
240
  if (!Array.isArray(sourcesContent) || sourcesContent.every((entry) => entry == null)) {
186
241
  warnings.push(
@@ -318,7 +373,7 @@ async function loadConfig() {
318
373
  };
319
374
  }
320
375
  try {
321
- const config = JSON.parse(await readFile(CONFIG_FILE, "utf8"));
376
+ const config = JSON.parse(await readFile2(CONFIG_FILE, "utf8"));
322
377
  if (!config.server || !config.adminToken) throw new Error("Incomplete config");
323
378
  return { server: normalizeServer(config.server), adminToken: config.adminToken };
324
379
  } catch {
@@ -344,7 +399,7 @@ async function request(server, token, path, options = {}) {
344
399
  }
345
400
  async function readPackageJson(cwd) {
346
401
  try {
347
- return JSON.parse(await readFile(join(cwd, "package.json"), "utf8"));
402
+ return JSON.parse(await readFile2(join2(cwd, "package.json"), "utf8"));
348
403
  } catch {
349
404
  throw new Error(
350
405
  "Run error-mom init from a JavaScript or TypeScript project containing package.json."
@@ -472,8 +527,8 @@ function detectFramework(packageJson) {
472
527
  };
473
528
  }
474
529
  function detectPackageManager(cwd) {
475
- if (existsSync(join(cwd, "pnpm-lock.yaml"))) return "pnpm";
476
- if (existsSync(join(cwd, "yarn.lock"))) return "yarn";
530
+ if (existsSync(join2(cwd, "pnpm-lock.yaml"))) return "pnpm";
531
+ if (existsSync(join2(cwd, "yarn.lock"))) return "yarn";
477
532
  return "npm";
478
533
  }
479
534
  function installSdk(packageManager) {
@@ -488,8 +543,8 @@ function installSdk(packageManager) {
488
543
  });
489
544
  }
490
545
  async function writeSetup(framework, server, ingestKey) {
491
- const sourceDirectory = existsSync(join(process.cwd(), "src")) ? "src" : ".";
492
- const relativePath = join(sourceDirectory, "error-mom.ts");
546
+ const sourceDirectory = existsSync(join2(process.cwd(), "src")) ? "src" : ".";
547
+ const relativePath = join2(sourceDirectory, "error-mom.ts");
493
548
  if (framework.id === "next") await writeNextInstrumentation(sourceDirectory, server, ingestKey);
494
549
  const environment = framework.envStyle === "next" ? "process.env.NEXT_PUBLIC_" : framework.envStyle === "vite" ? "import.meta.env.VITE_" : "process.env.";
495
550
  const moduleName = framework.envStyle === "node" ? "@kenkaiiii/error-mom/node" : "@kenkaiiii/error-mom";
@@ -508,11 +563,11 @@ export const errorMom = initErrorMom({
508
563
  ...(release ? { release } : {}),
509
564
  });
510
565
  `;
511
- await writeFile(join(process.cwd(), relativePath), contents);
566
+ await writeFile(join2(process.cwd(), relativePath), contents);
512
567
  return relativePath;
513
568
  }
514
569
  async function writeNextInstrumentation(sourceDirectory, configuredServer, ingestKey) {
515
- const file = join(process.cwd(), sourceDirectory, "instrumentation.ts");
570
+ const file = join2(process.cwd(), sourceDirectory, "instrumentation.ts");
516
571
  if (existsSync(file)) return;
517
572
  const contents = `import type { Instrumentation } from "next";
518
573
 
@@ -585,24 +640,6 @@ async function releaseMismatchWarning(config, projectIdOrSlug, release) {
585
640
  return [];
586
641
  }
587
642
  }
588
- async function findMapFiles(dir) {
589
- const found = [];
590
- let entries;
591
- try {
592
- entries = await readdir(dir, { withFileTypes: true });
593
- } catch {
594
- throw new Error(`Cannot read directory ${dir}.`);
595
- }
596
- for (const entry of entries) {
597
- const fullPath = join(dir, entry.name);
598
- if (entry.isDirectory() && entry.name !== "node_modules") {
599
- found.push(...await findMapFiles(fullPath));
600
- } else if (entry.isFile() && entry.name.endsWith(".map")) {
601
- found.push(fullPath);
602
- }
603
- }
604
- return found.sort();
605
- }
606
643
  function normalizeServer(server) {
607
644
  return server.replace(/\/$/, "");
608
645
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "error-mom",
3
- "version": "0.6.0",
3
+ "version": "0.7.1",
4
4
  "description": "Agent-first CLI and MCP tools for self-hosted Error Mom",
5
5
  "type": "module",
6
6
  "bin": {
@@ -11,11 +11,6 @@
11
11
  "dist",
12
12
  "README.md"
13
13
  ],
14
- "scripts": {
15
- "build": "tsup src/cli.ts --format esm --dts --clean",
16
- "check": "tsc --noEmit",
17
- "test": "vitest run --passWithNoTests"
18
- },
19
14
  "dependencies": {
20
15
  "@modelcontextprotocol/sdk": "^1.29.0",
21
16
  "commander": "^15.0.0",
@@ -28,5 +23,10 @@
28
23
  "publishConfig": {
29
24
  "access": "public"
30
25
  },
31
- "license": "MIT"
32
- }
26
+ "license": "MIT",
27
+ "scripts": {
28
+ "build": "tsup src/cli.ts --format esm --dts --clean",
29
+ "check": "tsc --noEmit",
30
+ "test": "vitest run --passWithNoTests"
31
+ }
32
+ }