error-mom 0.1.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/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 ADDED
@@ -0,0 +1,14 @@
1
+ # error-mom
2
+
3
+ Agent-first CLI and MCP tools for a self-hosted Error Mom incident desk.
4
+
5
+ ```bash
6
+ npm install --global error-mom
7
+ error-mom login https://errors.example.com --token "$ERROR_MOM_ADMIN_TOKEN"
8
+ error-mom projects
9
+ error-mom issues
10
+ error-mom inspect <issue-id>
11
+ error-mom resolve <issue-id> --release 1.4.2
12
+ ```
13
+
14
+ Run `error-mom mcp` as a stdio MCP server. It exposes `list_projects`, `list_issues`, `get_issue`, and `resolve_issue`.
package/dist/cli.d.ts ADDED
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
package/dist/cli.js ADDED
@@ -0,0 +1,342 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import { execFileSync } from "child_process";
5
+ import { chmod, mkdir, readFile, writeFile, appendFile } from "fs/promises";
6
+ import { existsSync } from "fs";
7
+ import { homedir } from "os";
8
+ import { basename, join } from "path";
9
+ import { Command } from "commander";
10
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
11
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
12
+ import { z } from "zod";
13
+ var VERSION = "0.1.0";
14
+ var CONFIG_DIR = join(homedir(), ".error-mom");
15
+ var CONFIG_FILE = join(CONFIG_DIR, "config.json");
16
+ var program = new Command().name("error-mom").description("Query and operate a self-hosted Error Mom incident desk").version(VERSION);
17
+ 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
+ const normalized = normalizeServer(server);
19
+ await request(normalized, options.token, "/api/v1/projects");
20
+ await mkdir(CONFIG_DIR, { recursive: true, mode: 448 });
21
+ await writeFile(
22
+ CONFIG_FILE,
23
+ `${JSON.stringify({ server: normalized, adminToken: options.token }, null, 2)}
24
+ `,
25
+ {
26
+ mode: 384
27
+ }
28
+ );
29
+ await chmod(CONFIG_FILE, 384);
30
+ print({ connected: true, server: normalized, configFile: CONFIG_FILE });
31
+ });
32
+ program.command("projects").description("List every project and unresolved issue count").action(async () => {
33
+ const config = await loadConfig();
34
+ print(await request(config.server, config.adminToken, "/api/v1/projects"));
35
+ });
36
+ program.command("issues").description("List compact issue summaries; unresolved issues are returned by default").option("--project <id>", "Filter by project ID").option("--status <status>", "unresolved, open, regressed, resolved, or all", "unresolved").action(async (options) => {
37
+ const config = await loadConfig();
38
+ const query = new URLSearchParams({ status: options.status });
39
+ if (options.project) query.set("projectId", options.project);
40
+ print(await request(config.server, config.adminToken, `/api/v1/issues?${query}`));
41
+ });
42
+ program.command("inspect").description("Fetch one issue with sampled stack traces, breadcrumbs, tags, and release spread").argument("<issue-id>").option("--samples <count>", "Representative samples to fetch (1-20)", "1").action(async (issueId, options) => {
43
+ const config = await loadConfig();
44
+ const samples = Math.min(Math.max(Number(options.samples) || 1, 1), 20);
45
+ print(
46
+ await request(
47
+ config.server,
48
+ config.adminToken,
49
+ `/api/v1/issues/${encodeURIComponent(issueId)}?samples=${samples}`
50
+ )
51
+ );
52
+ });
53
+ program.command("resolve").description("Resolve one issue and record the release containing the fix").argument("<issue-id>").requiredOption("--release <release>", "Release containing the fix").action(async (issueId, options) => {
54
+ const config = await loadConfig();
55
+ print(
56
+ await request(
57
+ config.server,
58
+ config.adminToken,
59
+ `/api/v1/issues/${encodeURIComponent(issueId)}`,
60
+ {
61
+ method: "PATCH",
62
+ body: { status: "resolved", fixedInRelease: options.release }
63
+ }
64
+ )
65
+ );
66
+ });
67
+ 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").action(async (options) => {
68
+ const config = await loadConfig();
69
+ const health = await request(config.server, void 0, "/api/health");
70
+ const projects = await request(config.server, config.adminToken, "/api/v1/projects");
71
+ let ingestion = "not tested";
72
+ if (options.projectKey) {
73
+ ingestion = await request(config.server, void 0, "/api/v1/events", {
74
+ headers: { "x-error-mom-key": options.projectKey },
75
+ body: {
76
+ events: [syntheticEvent()],
77
+ sdk: { name: "error-mom-cli-doctor", version: VERSION }
78
+ }
79
+ });
80
+ }
81
+ print({ healthy: true, health, projects, ingestion });
82
+ });
83
+ 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) => {
84
+ const config = await loadConfig();
85
+ const packageJson = await readPackageJson(process.cwd());
86
+ const name = options.name ?? (typeof packageJson.name === "string" ? packageJson.name : basename(process.cwd()));
87
+ const projectResponse = await request(
88
+ config.server,
89
+ config.adminToken,
90
+ "/api/v1/projects"
91
+ );
92
+ let project = options.project ? projectResponse.projects.find((candidate) => candidate.slug === options.project) : projectResponse.projects.find(
93
+ (candidate) => candidate.name.toLowerCase() === name.toLowerCase()
94
+ );
95
+ if (!project) {
96
+ const created = await request(config.server, config.adminToken, "/api/v1/projects", {
97
+ body: { name }
98
+ });
99
+ project = created.project;
100
+ }
101
+ if (!project.ingestKey) {
102
+ const createdKey = await request(
103
+ config.server,
104
+ config.adminToken,
105
+ `/api/v1/projects/${encodeURIComponent(project.id)}/ingest-keys`,
106
+ { body: {} }
107
+ );
108
+ project.ingestKey = createdKey.ingestKey;
109
+ }
110
+ const framework = detectFramework(packageJson);
111
+ if (!options.skipInstall) installSdk(detectPackageManager(process.cwd()));
112
+ const setupPath = await writeSetup(framework);
113
+ await writeFile(
114
+ join(process.cwd(), ".error-mom.json"),
115
+ `${JSON.stringify({ server: config.server, projectId: project.id, projectName: project.name, framework }, null, 2)}
116
+ `
117
+ );
118
+ await appendEnvironment(framework, config.server, project.ingestKey);
119
+ print({
120
+ installed: !options.skipInstall,
121
+ project: { id: project.id, name: project.name, slug: project.slug },
122
+ framework,
123
+ setupFile: setupPath,
124
+ verified: false,
125
+ nextAction: `Import ${setupPath} from the earliest ${framework} entry point, then run error-mom doctor --project-key <key>.`
126
+ });
127
+ });
128
+ program.command("mcp").description("Run Error Mom tools over MCP stdio for coding agents").action(async () => {
129
+ await runMcpServer();
130
+ });
131
+ program.parseAsync().catch((error) => {
132
+ process.stderr.write(
133
+ `${JSON.stringify({ error: { message: error instanceof Error ? error.message : String(error) } }, null, 2)}
134
+ `
135
+ );
136
+ process.exitCode = 1;
137
+ });
138
+ async function runMcpServer() {
139
+ const config = await loadConfig();
140
+ const server = new McpServer({ name: "error-mom", version: VERSION });
141
+ server.registerTool(
142
+ "list_projects",
143
+ { description: "List Error Mom projects and unresolved issue counts", inputSchema: {} },
144
+ async () => toolResult(await request(config.server, config.adminToken, "/api/v1/projects"))
145
+ );
146
+ server.registerTool(
147
+ "list_issues",
148
+ {
149
+ description: "List compact issues. Defaults to unresolved so fixed work does not consume context.",
150
+ inputSchema: {
151
+ projectId: z.string().optional(),
152
+ status: z.enum(["unresolved", "open", "regressed", "resolved", "all"]).default("unresolved")
153
+ }
154
+ },
155
+ async ({ projectId, status }) => {
156
+ const query = new URLSearchParams({ status });
157
+ if (projectId) query.set("projectId", projectId);
158
+ return toolResult(await request(config.server, config.adminToken, `/api/v1/issues?${query}`));
159
+ }
160
+ );
161
+ server.registerTool(
162
+ "get_issue",
163
+ {
164
+ description: "Inspect one issue with representative evidence. Fetch one sample by default to protect context.",
165
+ inputSchema: {
166
+ issueId: z.string().min(1),
167
+ samples: z.number().int().min(1).max(20).default(1)
168
+ }
169
+ },
170
+ async ({ issueId, samples }) => toolResult(
171
+ await request(
172
+ config.server,
173
+ config.adminToken,
174
+ `/api/v1/issues/${encodeURIComponent(issueId)}?samples=${samples}`
175
+ )
176
+ )
177
+ );
178
+ server.registerTool(
179
+ "resolve_issue",
180
+ {
181
+ description: "Mark an issue fixed in a release. A recurrence in that release reopens it as a regression.",
182
+ inputSchema: { issueId: z.string().min(1), fixedInRelease: z.string().min(1) }
183
+ },
184
+ async ({ issueId, fixedInRelease }) => toolResult(
185
+ await request(
186
+ config.server,
187
+ config.adminToken,
188
+ `/api/v1/issues/${encodeURIComponent(issueId)}`,
189
+ {
190
+ method: "PATCH",
191
+ body: { status: "resolved", fixedInRelease }
192
+ }
193
+ )
194
+ )
195
+ );
196
+ await server.connect(new StdioServerTransport());
197
+ }
198
+ function toolResult(value) {
199
+ return { content: [{ type: "text", text: JSON.stringify(value, null, 2) }] };
200
+ }
201
+ async function loadConfig() {
202
+ const fromEnvironment = {
203
+ server: process.env.ERROR_MOM_SERVER,
204
+ adminToken: process.env.ERROR_MOM_ADMIN_TOKEN
205
+ };
206
+ if (fromEnvironment.server && fromEnvironment.adminToken) {
207
+ return {
208
+ server: normalizeServer(fromEnvironment.server),
209
+ adminToken: fromEnvironment.adminToken
210
+ };
211
+ }
212
+ try {
213
+ const config = JSON.parse(await readFile(CONFIG_FILE, "utf8"));
214
+ if (!config.server || !config.adminToken) throw new Error("Incomplete config");
215
+ return { server: normalizeServer(config.server), adminToken: config.adminToken };
216
+ } catch {
217
+ throw new Error(
218
+ `Run error-mom login <server> --token <token>, or set ERROR_MOM_SERVER and ERROR_MOM_ADMIN_TOKEN.`
219
+ );
220
+ }
221
+ }
222
+ async function request(server, token, path, options = {}) {
223
+ const response = await fetch(`${normalizeServer(server)}${path}`, {
224
+ method: options.method ?? (options.body ? "POST" : "GET"),
225
+ headers: {
226
+ ...options.body ? { "content-type": "application/json" } : {},
227
+ ...token ? { authorization: `Bearer ${token}` } : {},
228
+ ...options.headers
229
+ },
230
+ ...options.body ? { body: JSON.stringify(options.body) } : {}
231
+ });
232
+ const result = await response.json().catch(() => ({ error: { message: response.statusText } }));
233
+ if (!response.ok)
234
+ throw new Error(result.error?.message ?? `Error Mom returned HTTP ${response.status}`);
235
+ return result;
236
+ }
237
+ async function readPackageJson(cwd) {
238
+ try {
239
+ return JSON.parse(await readFile(join(cwd, "package.json"), "utf8"));
240
+ } catch {
241
+ throw new Error(
242
+ "Run error-mom init from a JavaScript or TypeScript project containing package.json."
243
+ );
244
+ }
245
+ }
246
+ function detectFramework(packageJson) {
247
+ const dependencies = {
248
+ ...packageJson.dependencies ?? {},
249
+ ...packageJson.devDependencies ?? {}
250
+ };
251
+ if (dependencies.next) return "next";
252
+ if (dependencies.vite) return "vite";
253
+ return "node";
254
+ }
255
+ function detectPackageManager(cwd) {
256
+ if (existsSync(join(cwd, "pnpm-lock.yaml"))) return "pnpm";
257
+ if (existsSync(join(cwd, "yarn.lock"))) return "yarn";
258
+ return "npm";
259
+ }
260
+ function installSdk(packageManager) {
261
+ const argumentsByManager = {
262
+ pnpm: ["add", "@kenkaiiii/error-mom"],
263
+ yarn: ["add", "@kenkaiiii/error-mom"],
264
+ npm: ["install", "@kenkaiiii/error-mom"]
265
+ };
266
+ execFileSync(packageManager, argumentsByManager[packageManager], {
267
+ cwd: process.cwd(),
268
+ stdio: "inherit"
269
+ });
270
+ }
271
+ async function writeSetup(framework) {
272
+ const sourceDirectory = existsSync(join(process.cwd(), "src")) ? "src" : ".";
273
+ const relativePath = join(sourceDirectory, "error-mom.ts");
274
+ const environment = framework === "next" ? "process.env.NEXT_PUBLIC_" : framework === "vite" ? "import.meta.env.VITE_" : "process.env.";
275
+ const moduleName = framework === "node" ? "@kenkaiiii/error-mom/node" : "@kenkaiiii/error-mom";
276
+ const contents = `${framework === "next" ? '"use client";\n\n' : ""}import { initErrorMom } from "${moduleName}";
277
+
278
+ const server = ${environment}ERROR_MOM_SERVER;
279
+ const projectKey = ${environment}ERROR_MOM_PROJECT_KEY;
280
+ const release = ${environment}ERROR_MOM_RELEASE;
281
+
282
+ if (!server || !projectKey) {
283
+ throw new Error("ERROR_MOM_SERVER and ERROR_MOM_PROJECT_KEY are required");
284
+ }
285
+
286
+ initErrorMom({
287
+ server,
288
+ projectKey,
289
+ environment: ${environment}ERROR_MOM_ENVIRONMENT ?? "production",
290
+ ...(release ? { release } : {}),
291
+ });
292
+ `;
293
+ await writeFile(join(process.cwd(), relativePath), contents);
294
+ return relativePath;
295
+ }
296
+ async function appendEnvironment(framework, server, key) {
297
+ const prefix = framework === "next" ? "NEXT_PUBLIC_" : framework === "vite" ? "VITE_" : "";
298
+ const file = join(process.cwd(), ".env.local");
299
+ const existing = existsSync(file) ? await readFile(file, "utf8") : "";
300
+ const lines = [
301
+ [`${prefix}ERROR_MOM_SERVER`, server],
302
+ [`${prefix}ERROR_MOM_PROJECT_KEY`, key],
303
+ [`${prefix}ERROR_MOM_ENVIRONMENT`, "production"]
304
+ ].filter(([name]) => !existing.includes(`${name}=`));
305
+ if (lines.length) {
306
+ await appendFile(
307
+ file,
308
+ `${existing && !existing.endsWith("\n") ? "\n" : ""}${lines.map(([name, value]) => `${name}=${value}`).join("\n")}
309
+ `,
310
+ {
311
+ mode: 384
312
+ }
313
+ );
314
+ }
315
+ }
316
+ function syntheticEvent() {
317
+ return {
318
+ eventId: crypto.randomUUID(),
319
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
320
+ level: "error",
321
+ error: {
322
+ name: "ErrorMomDoctor",
323
+ message: "Synthetic setup verification",
324
+ stack: "ErrorMomDoctor: Synthetic setup verification\\n at error-mom doctor:1:1"
325
+ },
326
+ environment: "setup",
327
+ release: VERSION,
328
+ platform: process.platform,
329
+ runtime: `node ${process.version}`,
330
+ culprit: "error-mom doctor",
331
+ breadcrumbs: [],
332
+ tags: { synthetic: "true" },
333
+ context: {}
334
+ };
335
+ }
336
+ function normalizeServer(server) {
337
+ return server.replace(/\/$/, "");
338
+ }
339
+ function print(value) {
340
+ process.stdout.write(`${JSON.stringify(value, null, 2)}
341
+ `);
342
+ }
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "error-mom",
3
+ "version": "0.1.0",
4
+ "description": "Agent-first CLI and MCP tools for self-hosted Error Mom",
5
+ "type": "module",
6
+ "bin": {
7
+ "error-mom": "./dist/cli.js"
8
+ },
9
+ "main": "./dist/cli.js",
10
+ "files": [
11
+ "dist",
12
+ "README.md"
13
+ ],
14
+ "dependencies": {
15
+ "@modelcontextprotocol/sdk": "^1.29.0",
16
+ "commander": "^15.0.0",
17
+ "zod": "^4.4.3"
18
+ },
19
+ "devDependencies": {
20
+ "tsup": "^8.5.1",
21
+ "vitest": "^4.1.10"
22
+ },
23
+ "publishConfig": {
24
+ "access": "public"
25
+ },
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
+ }