error-mom 0.4.1 → 0.6.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/dist/cli.js +133 -4
- package/package.json +8 -8
- package/LICENSE +0 -21
package/dist/cli.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
4
|
import { execFileSync } from "child_process";
|
|
5
|
-
import { chmod, mkdir, readFile, writeFile } from "fs/promises";
|
|
5
|
+
import { chmod, mkdir, readFile, readdir, stat, writeFile } from "fs/promises";
|
|
6
6
|
import { existsSync } from "fs";
|
|
7
7
|
import { homedir } from "os";
|
|
8
8
|
import { basename, join } from "path";
|
|
@@ -10,7 +10,7 @@ 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.
|
|
13
|
+
var VERSION = "0.6.0";
|
|
14
14
|
var CONFIG_DIR = join(homedir(), ".error-mom");
|
|
15
15
|
var CONFIG_FILE = join(CONFIG_DIR, "config.json");
|
|
16
16
|
var program = new Command().name("error-mom").description("Query and operate a self-hosted Error Mom incident desk").version(VERSION);
|
|
@@ -75,7 +75,10 @@ program.command("delete-project").description("Permanently delete one project an
|
|
|
75
75
|
)
|
|
76
76
|
);
|
|
77
77
|
});
|
|
78
|
-
program.command("doctor").description("Verify collector health, credentials, and optional project ingestion").option("--project-key <key>", "Send and verify a synthetic event with this write-only key").
|
|
78
|
+
program.command("doctor").description("Verify collector health, credentials, and optional project ingestion").option("--project-key <key>", "Send and verify a synthetic event with this write-only key").option(
|
|
79
|
+
"--symbolication",
|
|
80
|
+
"Round-trip a synthetic minified stack through the server's symbolication engine"
|
|
81
|
+
).action(async (options) => {
|
|
79
82
|
const config = await loadConfig();
|
|
80
83
|
const health = await request(config.server, void 0, "/api/health");
|
|
81
84
|
const projects = await request(config.server, config.adminToken, "/api/v1/projects");
|
|
@@ -89,7 +92,30 @@ program.command("doctor").description("Verify collector health, credentials, and
|
|
|
89
92
|
}
|
|
90
93
|
});
|
|
91
94
|
}
|
|
92
|
-
|
|
95
|
+
let symbolication = "not tested";
|
|
96
|
+
if (options.symbolication) {
|
|
97
|
+
const result = await request(config.server, config.adminToken, "/api/v1/sourcemaps/check", {
|
|
98
|
+
body: {
|
|
99
|
+
stack: "ErrorMomDoctor: synthetic minified stack\n at t.xyz (https://app.example/assets/doctor-abc.js:1:11)",
|
|
100
|
+
fileName: "doctor-abc.js",
|
|
101
|
+
map: {
|
|
102
|
+
version: 3,
|
|
103
|
+
file: "doctor-abc.js",
|
|
104
|
+
sources: ["src/doctor-fixture.ts"],
|
|
105
|
+
names: ["doctorBoom"],
|
|
106
|
+
mappings: "UAIEA"
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
const roundTripped = result.symbolicated === true && typeof result.stack === "string" && result.stack.includes("doctorBoom (src/doctor-fixture.ts:5:3)");
|
|
111
|
+
if (!roundTripped) {
|
|
112
|
+
throw new Error(
|
|
113
|
+
`Symbolication round-trip failed: expected the synthetic frame to rewrite to src/doctor-fixture.ts:5:3, got ${JSON.stringify(result)}`
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
symbolication = { verified: true, rewrittenFrame: "doctorBoom (src/doctor-fixture.ts:5:3)" };
|
|
117
|
+
}
|
|
118
|
+
print({ healthy: true, health, projects, ingestion, symbolication });
|
|
93
119
|
});
|
|
94
120
|
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) => {
|
|
95
121
|
const config = await loadConfig();
|
|
@@ -132,6 +158,50 @@ program.command("init").description("Create/select a project, install the SDK, a
|
|
|
132
158
|
nextAction: `Wire it up: ${framework.wiring} If the app routes caught errors through a central handler or error-broadcast function, call errorMom.captureError(err) inside it \u2014 that is where framework-caught and LLM errors surface. For handlers where a framework catches errors itself (queue/cron jobs, webhooks, MCP tools), wrap each with errorMom.wrap(fn, { culprit: "<name>" }). The setup file has the write-only project key baked in (safe to commit; ERROR_MOM_* env vars override when set), so production builds report without any configuration. Then run error-mom doctor --project-key ${project.ingestKey}.`
|
|
133
159
|
});
|
|
134
160
|
});
|
|
161
|
+
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
|
+
const config = await loadConfig();
|
|
163
|
+
const mapFiles = await findMapFiles(dir);
|
|
164
|
+
if (mapFiles.length === 0) {
|
|
165
|
+
throw new Error(`No .map files found under ${dir}. Build with source maps enabled first.`);
|
|
166
|
+
}
|
|
167
|
+
const uploaded = [];
|
|
168
|
+
const skipped = [];
|
|
169
|
+
const warnings = [];
|
|
170
|
+
for (const mapFile of mapFiles) {
|
|
171
|
+
const info = await stat(mapFile);
|
|
172
|
+
if (info.size > 20 * 1024 * 1024) {
|
|
173
|
+
skipped.push({ file: mapFile, reason: "larger than 20 MB" });
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
let map;
|
|
177
|
+
try {
|
|
178
|
+
map = JSON.parse(await readFile(mapFile, "utf8"));
|
|
179
|
+
} catch {
|
|
180
|
+
skipped.push({ file: mapFile, reason: "not valid JSON" });
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
const fileName = basename(mapFile).replace(/\.map$/, "");
|
|
184
|
+
const sourcesContent = map.sourcesContent;
|
|
185
|
+
if (!Array.isArray(sourcesContent) || sourcesContent.every((entry) => entry == null)) {
|
|
186
|
+
warnings.push(
|
|
187
|
+
`${fileName}: no sourcesContent \u2014 frames still symbolicate to file:line, but agents cannot read the original code from the map. Enable it in the bundler if you want richer context.`
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
try {
|
|
191
|
+
await request(config.server, config.adminToken, "/api/v1/sourcemaps", {
|
|
192
|
+
body: { projectId: options.project, release: options.release, fileName, map }
|
|
193
|
+
});
|
|
194
|
+
uploaded.push(fileName);
|
|
195
|
+
} catch (error) {
|
|
196
|
+
skipped.push({
|
|
197
|
+
file: mapFile,
|
|
198
|
+
reason: error instanceof Error ? error.message : String(error)
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
warnings.push(...await releaseMismatchWarning(config, options.project, options.release));
|
|
203
|
+
print({ release: options.release, project: options.project, uploaded, skipped, warnings });
|
|
204
|
+
});
|
|
135
205
|
program.command("mcp").description("Run Error Mom tools over MCP stdio for coding agents").action(async () => {
|
|
136
206
|
await runMcpServer();
|
|
137
207
|
});
|
|
@@ -200,6 +270,22 @@ async function runMcpServer() {
|
|
|
200
270
|
)
|
|
201
271
|
)
|
|
202
272
|
);
|
|
273
|
+
server.registerTool(
|
|
274
|
+
"check_symbolication",
|
|
275
|
+
{
|
|
276
|
+
description: "Dry-run a minified stack against a project's uploaded source maps to verify frames rewrite to original file:line. Nothing is stored.",
|
|
277
|
+
inputSchema: {
|
|
278
|
+
projectId: z.string().min(1).describe("Project id or slug"),
|
|
279
|
+
release: z.string().min(1),
|
|
280
|
+
stack: z.string().min(1).max(5e4)
|
|
281
|
+
}
|
|
282
|
+
},
|
|
283
|
+
async ({ projectId, release, stack }) => toolResult(
|
|
284
|
+
await request(config.server, config.adminToken, "/api/v1/sourcemaps/check", {
|
|
285
|
+
body: { projectId, release, stack }
|
|
286
|
+
})
|
|
287
|
+
)
|
|
288
|
+
);
|
|
203
289
|
server.registerTool(
|
|
204
290
|
"delete_project",
|
|
205
291
|
{
|
|
@@ -474,6 +560,49 @@ function syntheticEvent() {
|
|
|
474
560
|
context: {}
|
|
475
561
|
};
|
|
476
562
|
}
|
|
563
|
+
async function releaseMismatchWarning(config, projectIdOrSlug, release) {
|
|
564
|
+
try {
|
|
565
|
+
const { projects } = await request(config.server, config.adminToken, "/api/v1/projects");
|
|
566
|
+
const project = projects.find(
|
|
567
|
+
(candidate) => candidate.id === projectIdOrSlug || candidate.slug === projectIdOrSlug
|
|
568
|
+
);
|
|
569
|
+
if (!project) return [];
|
|
570
|
+
const { issues } = await request(
|
|
571
|
+
config.server,
|
|
572
|
+
config.adminToken,
|
|
573
|
+
`/api/v1/issues?${new URLSearchParams({ projectId: project.id, status: "all" })}`
|
|
574
|
+
);
|
|
575
|
+
const seenReleases = [
|
|
576
|
+
...new Set(issues.map((issue) => issue.latestRelease).filter((value) => value !== null))
|
|
577
|
+
];
|
|
578
|
+
if (seenReleases.length > 0 && !seenReleases.includes(release)) {
|
|
579
|
+
return [
|
|
580
|
+
`release "${release}" has not been reported by any event yet (recent releases: ${seenReleases.slice(0, 5).join(", ")}). Maps only apply when the SDK's release matches --release exactly.`
|
|
581
|
+
];
|
|
582
|
+
}
|
|
583
|
+
return [];
|
|
584
|
+
} catch {
|
|
585
|
+
return [];
|
|
586
|
+
}
|
|
587
|
+
}
|
|
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
|
+
}
|
|
477
606
|
function normalizeServer(server) {
|
|
478
607
|
return server.replace(/\/$/, "");
|
|
479
608
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "error-mom",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Agent-first CLI and MCP tools for self-hosted Error Mom",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -11,6 +11,11 @@
|
|
|
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
|
+
},
|
|
14
19
|
"dependencies": {
|
|
15
20
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
16
21
|
"commander": "^15.0.0",
|
|
@@ -23,10 +28,5 @@
|
|
|
23
28
|
"publishConfig": {
|
|
24
29
|
"access": "public"
|
|
25
30
|
},
|
|
26
|
-
"license": "MIT"
|
|
27
|
-
|
|
28
|
-
"build": "tsup src/cli.ts --format esm --dts --clean",
|
|
29
|
-
"check": "tsc --noEmit",
|
|
30
|
-
"test": "vitest run --passWithNoTests"
|
|
31
|
-
}
|
|
32
|
-
}
|
|
31
|
+
"license": "MIT"
|
|
32
|
+
}
|
package/LICENSE
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
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.
|