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.
- package/LICENSE +21 -0
- package/README.md +92 -2
- package/dist/cli.js +75 -38
- 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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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 :
|
|
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
|
|
164
|
-
if (
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
476
|
-
if (existsSync(
|
|
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(
|
|
492
|
-
const relativePath =
|
|
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(
|
|
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 =
|
|
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.
|
|
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
|
+
}
|