@sweny-ai/core 0.1.67 → 0.1.69
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/__tests__/diagram.test.js +84 -60
- package/dist/cli/__tests__/report-cloud.test.js +1 -1
- package/dist/cli/__tests__/upgrade.test.js +11 -0
- package/dist/cli/__tests__/version-check.test.js +17 -0
- package/dist/cli/check.js +18 -15
- package/dist/cli/config.d.ts +2 -2
- package/dist/cli/config.js +128 -119
- package/dist/cli/config.test.js +11 -7
- package/dist/cli/diagram.d.ts +27 -8
- package/dist/cli/diagram.js +35 -19
- package/dist/cli/main.js +33 -27
- package/dist/cli/output.js +3 -3
- package/dist/cli/output.test.js +1 -1
- package/dist/mcp.d.ts +1 -1
- package/dist/mcp.js +13 -19
- package/dist/mcp.test.js +12 -12
- package/dist/types.d.ts +2 -1
- package/package.json +1 -1
|
@@ -2,8 +2,7 @@ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
|
2
2
|
import * as fs from "node:fs";
|
|
3
3
|
import * as os from "node:os";
|
|
4
4
|
import * as path from "node:path";
|
|
5
|
-
import {
|
|
6
|
-
import { runWorkflowDiagram } from "../diagram.js";
|
|
5
|
+
import { runWorkflowDiagram, resolveUseBlock } from "../diagram.js";
|
|
7
6
|
const sampleWorkflow = {
|
|
8
7
|
id: "sample",
|
|
9
8
|
name: "Sample",
|
|
@@ -32,6 +31,42 @@ function captureStream() {
|
|
|
32
31
|
},
|
|
33
32
|
};
|
|
34
33
|
}
|
|
34
|
+
describe("resolveUseBlock", () => {
|
|
35
|
+
// Centralized behavior test — the renderer is a tiny switch around this
|
|
36
|
+
// so keeping the rules here as one table makes the policy easy to audit.
|
|
37
|
+
it("returns false by default (stdout, no -o, no flags)", () => {
|
|
38
|
+
expect(resolveUseBlock({})).toBe(false);
|
|
39
|
+
});
|
|
40
|
+
it("returns true when --block is explicit", () => {
|
|
41
|
+
expect(resolveUseBlock({ block: true })).toBe(true);
|
|
42
|
+
});
|
|
43
|
+
it("returns false when --no-block is explicit, even for .md", () => {
|
|
44
|
+
expect(resolveUseBlock({ block: false, output: "diagram.md" })).toBe(false);
|
|
45
|
+
});
|
|
46
|
+
it("infers true for .md output", () => {
|
|
47
|
+
expect(resolveUseBlock({ output: "diagram.md" })).toBe(true);
|
|
48
|
+
});
|
|
49
|
+
it("infers true for .markdown output", () => {
|
|
50
|
+
expect(resolveUseBlock({ output: "diagram.markdown" })).toBe(true);
|
|
51
|
+
});
|
|
52
|
+
it("infers false for .mmd output", () => {
|
|
53
|
+
expect(resolveUseBlock({ output: "diagram.mmd" })).toBe(false);
|
|
54
|
+
});
|
|
55
|
+
it("infers false for .mermaid output", () => {
|
|
56
|
+
expect(resolveUseBlock({ output: "diagram.mermaid" })).toBe(false);
|
|
57
|
+
});
|
|
58
|
+
it("infers false for any other extension (.txt, no ext, etc.)", () => {
|
|
59
|
+
expect(resolveUseBlock({ output: "diagram.txt" })).toBe(false);
|
|
60
|
+
expect(resolveUseBlock({ output: "diagram" })).toBe(false);
|
|
61
|
+
});
|
|
62
|
+
it("is case-insensitive on extension", () => {
|
|
63
|
+
expect(resolveUseBlock({ output: "Diagram.MD" })).toBe(true);
|
|
64
|
+
expect(resolveUseBlock({ output: "Diagram.MMD" })).toBe(false);
|
|
65
|
+
});
|
|
66
|
+
it("lets explicit --block override .mmd extension", () => {
|
|
67
|
+
expect(resolveUseBlock({ block: true, output: "diagram.mmd" })).toBe(true);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
35
70
|
describe("runWorkflowDiagram", () => {
|
|
36
71
|
let tmpdir;
|
|
37
72
|
const loadWorkflowFile = vi.fn((_f) => sampleWorkflow);
|
|
@@ -44,55 +79,78 @@ describe("runWorkflowDiagram", () => {
|
|
|
44
79
|
afterEach(() => {
|
|
45
80
|
fs.rmSync(tmpdir, { recursive: true, force: true });
|
|
46
81
|
});
|
|
47
|
-
describe("stdout", () => {
|
|
48
|
-
it("writes
|
|
82
|
+
describe("stdout — default behavior", () => {
|
|
83
|
+
it("writes RAW mermaid (no fence, no title frontmatter) by default", () => {
|
|
84
|
+
// This is the Mermaid-CLI / Live-Editor-compatible output. No ```,
|
|
85
|
+
// no `---\ntitle: …\n---` — just `graph TB` and the nodes.
|
|
49
86
|
const stdout = captureStream();
|
|
50
87
|
const stderr = captureStream();
|
|
51
88
|
runWorkflowDiagram("any.yml", {}, { loadWorkflowFile, stdout: stdout.stream, stderr: stderr.stream, exit });
|
|
52
|
-
expect(stdout.text).toMatch(
|
|
53
|
-
expect(stdout.text).toMatch(
|
|
89
|
+
expect(stdout.text).not.toMatch(/```mermaid/);
|
|
90
|
+
expect(stdout.text).not.toMatch(/^---\ntitle:/);
|
|
91
|
+
expect(stdout.text).toMatch(/^graph TB/);
|
|
54
92
|
expect(exit).not.toHaveBeenCalled();
|
|
55
93
|
});
|
|
56
|
-
it("
|
|
94
|
+
it("wraps in a ```mermaid fence when --block is explicit", () => {
|
|
57
95
|
const stdout = captureStream();
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
expect(stdout.text).
|
|
61
|
-
expect(stdout.text).toMatch(/graph TB/);
|
|
96
|
+
runWorkflowDiagram("any.yml", { block: true }, { loadWorkflowFile, stdout: stdout.stream, stderr: captureStream().stream, exit });
|
|
97
|
+
expect(stdout.text).toMatch(/^```mermaid\n/);
|
|
98
|
+
expect(stdout.text).toMatch(/\n```\n$/);
|
|
62
99
|
});
|
|
63
100
|
it("honors --direction LR", () => {
|
|
64
101
|
const stdout = captureStream();
|
|
65
102
|
runWorkflowDiagram("any.yml", { direction: "LR" }, { loadWorkflowFile, stdout: stdout.stream, stderr: captureStream().stream, exit });
|
|
66
103
|
expect(stdout.text).toMatch(/graph LR/);
|
|
67
104
|
});
|
|
68
|
-
it("
|
|
105
|
+
it("emits --title as frontmatter only when explicitly set", () => {
|
|
69
106
|
const stdout = captureStream();
|
|
70
107
|
runWorkflowDiagram("any.yml", { title: "Custom Title" }, { loadWorkflowFile, stdout: stdout.stream, stderr: captureStream().stream, exit });
|
|
71
|
-
expect(stdout.text).
|
|
108
|
+
expect(stdout.text).toMatch(/^---\ntitle: Custom Title\n---\n/);
|
|
109
|
+
expect(stdout.text).toContain("graph TB");
|
|
110
|
+
});
|
|
111
|
+
it("does NOT default title to the workflow name", () => {
|
|
112
|
+
// Regression guard: previously we auto-filled `title` from workflow.name,
|
|
113
|
+
// which broke mmdc / mermaid-live because they don't accept title frontmatter.
|
|
114
|
+
const stdout = captureStream();
|
|
115
|
+
runWorkflowDiagram("any.yml", {}, { loadWorkflowFile, stdout: stdout.stream, stderr: captureStream().stream, exit });
|
|
116
|
+
expect(stdout.text).not.toMatch(/title:/);
|
|
117
|
+
expect(stdout.text).not.toContain("Sample"); // workflow.name
|
|
72
118
|
});
|
|
73
119
|
});
|
|
74
120
|
describe("-o / --output file", () => {
|
|
75
|
-
it("writes raw mermaid to .mmd
|
|
121
|
+
it("writes raw mermaid to .mmd (default)", () => {
|
|
76
122
|
const out = path.join(tmpdir, "diagram.mmd");
|
|
77
123
|
const stderr = captureStream();
|
|
78
124
|
runWorkflowDiagram("any.yml", { output: out }, { loadWorkflowFile, stdout: captureStream().stream, stderr: stderr.stream, exit });
|
|
79
125
|
const contents = fs.readFileSync(out, "utf8");
|
|
80
126
|
expect(contents).not.toMatch(/```mermaid/);
|
|
81
|
-
expect(contents).toMatch(/
|
|
127
|
+
expect(contents).not.toMatch(/title:/);
|
|
128
|
+
expect(contents).toMatch(/^graph TB/);
|
|
82
129
|
expect(stderr.text).toMatch(/✓ Wrote diagram to/);
|
|
83
130
|
});
|
|
84
|
-
it("writes
|
|
131
|
+
it("writes raw mermaid to .mermaid (Mermaid CLI's canonical extension)", () => {
|
|
132
|
+
const out = path.join(tmpdir, "diagram.mermaid");
|
|
133
|
+
runWorkflowDiagram("any.yml", { output: out }, { loadWorkflowFile, stdout: captureStream().stream, stderr: captureStream().stream, exit });
|
|
134
|
+
const contents = fs.readFileSync(out, "utf8");
|
|
135
|
+
expect(contents).not.toMatch(/```mermaid/);
|
|
136
|
+
expect(contents).toMatch(/^graph TB/);
|
|
137
|
+
});
|
|
138
|
+
it("writes fenced markdown to .md", () => {
|
|
85
139
|
const out = path.join(tmpdir, "diagram.md");
|
|
86
140
|
runWorkflowDiagram("any.yml", { output: out }, { loadWorkflowFile, stdout: captureStream().stream, stderr: captureStream().stream, exit });
|
|
87
141
|
const contents = fs.readFileSync(out, "utf8");
|
|
88
142
|
expect(contents).toMatch(/^```mermaid\n/);
|
|
89
143
|
expect(contents).toMatch(/\n```\n$/);
|
|
90
144
|
});
|
|
91
|
-
it("
|
|
145
|
+
it("writes raw mermaid for unknown extensions (no longer falls back to fenced)", () => {
|
|
146
|
+
// Behavior change: previously .txt and friends got fenced as the
|
|
147
|
+
// "safe default". Now raw mermaid is the safe default because that's
|
|
148
|
+
// what every renderer actually accepts.
|
|
92
149
|
const out = path.join(tmpdir, "diagram.txt");
|
|
93
150
|
runWorkflowDiagram("any.yml", { output: out }, { loadWorkflowFile, stdout: captureStream().stream, stderr: captureStream().stream, exit });
|
|
94
151
|
const contents = fs.readFileSync(out, "utf8");
|
|
95
|
-
expect(contents).toMatch(/```mermaid/);
|
|
152
|
+
expect(contents).not.toMatch(/```mermaid/);
|
|
153
|
+
expect(contents).toMatch(/graph TB/);
|
|
96
154
|
});
|
|
97
155
|
it("creates parent directories as needed (-o nested/subdir/file.mmd)", () => {
|
|
98
156
|
const out = path.join(tmpdir, "a", "b", "c", "diagram.mmd");
|
|
@@ -107,51 +165,15 @@ describe("runWorkflowDiagram", () => {
|
|
|
107
165
|
expect(stdout.text).toBe("");
|
|
108
166
|
});
|
|
109
167
|
});
|
|
110
|
-
describe("explicit --block / --no-block
|
|
111
|
-
|
|
112
|
-
const cmd = new Command();
|
|
113
|
-
cmd.option("--block", "fenced", true);
|
|
114
|
-
cmd.option("--no-block", "raw");
|
|
115
|
-
if (explicit) {
|
|
116
|
-
// Simulate user typing `--block` or `--no-block` on the CLI — commander
|
|
117
|
-
// marks the source as "cli" when the flag is seen in argv.
|
|
118
|
-
cmd.setOptionValueWithSource("block", block, "cli");
|
|
119
|
-
}
|
|
120
|
-
return cmd;
|
|
121
|
-
}
|
|
122
|
-
it("explicit --block wins over .mmd extension (writes fenced to .mmd)", () => {
|
|
168
|
+
describe("explicit --block / --no-block override extension inference", () => {
|
|
169
|
+
it("explicit --block wins over .mmd (writes fenced to .mmd)", () => {
|
|
123
170
|
const out = path.join(tmpdir, "diagram.mmd");
|
|
124
|
-
runWorkflowDiagram("any.yml", { output: out, block: true }, {
|
|
125
|
-
loadWorkflowFile,
|
|
126
|
-
command: cmdWithBlock(true, true),
|
|
127
|
-
stdout: captureStream().stream,
|
|
128
|
-
stderr: captureStream().stream,
|
|
129
|
-
exit,
|
|
130
|
-
});
|
|
171
|
+
runWorkflowDiagram("any.yml", { output: out, block: true }, { loadWorkflowFile, stdout: captureStream().stream, stderr: captureStream().stream, exit });
|
|
131
172
|
expect(fs.readFileSync(out, "utf8")).toMatch(/```mermaid/);
|
|
132
173
|
});
|
|
133
|
-
it("explicit --no-block wins over .md
|
|
174
|
+
it("explicit --no-block wins over .md (writes raw to .md)", () => {
|
|
134
175
|
const out = path.join(tmpdir, "diagram.md");
|
|
135
|
-
runWorkflowDiagram("any.yml", { output: out, block: false }, {
|
|
136
|
-
loadWorkflowFile,
|
|
137
|
-
command: cmdWithBlock(false, true),
|
|
138
|
-
stdout: captureStream().stream,
|
|
139
|
-
stderr: captureStream().stream,
|
|
140
|
-
exit,
|
|
141
|
-
});
|
|
142
|
-
expect(fs.readFileSync(out, "utf8")).not.toMatch(/```mermaid/);
|
|
143
|
-
});
|
|
144
|
-
it("default --block source does not suppress .mmd extension inference", () => {
|
|
145
|
-
// Commander always sets `block: true` via the declared default, but its
|
|
146
|
-
// source is "default" — our inference should still kick in for .mmd.
|
|
147
|
-
const out = path.join(tmpdir, "diagram.mmd");
|
|
148
|
-
runWorkflowDiagram("any.yml", { output: out, block: true }, {
|
|
149
|
-
loadWorkflowFile,
|
|
150
|
-
command: cmdWithBlock(true, false), // not explicit
|
|
151
|
-
stdout: captureStream().stream,
|
|
152
|
-
stderr: captureStream().stream,
|
|
153
|
-
exit,
|
|
154
|
-
});
|
|
176
|
+
runWorkflowDiagram("any.yml", { output: out, block: false }, { loadWorkflowFile, stdout: captureStream().stream, stderr: captureStream().stream, exit });
|
|
155
177
|
expect(fs.readFileSync(out, "utf8")).not.toMatch(/```mermaid/);
|
|
156
178
|
});
|
|
157
179
|
});
|
|
@@ -160,7 +182,9 @@ describe("runWorkflowDiagram", () => {
|
|
|
160
182
|
const stdout = captureStream();
|
|
161
183
|
runWorkflowDiagram("triage", {}, { loadWorkflowFile, stdout: stdout.stream, stderr: captureStream().stream, exit });
|
|
162
184
|
expect(loadWorkflowFile).not.toHaveBeenCalled();
|
|
163
|
-
expect(stdout.text).toMatch(
|
|
185
|
+
expect(stdout.text).toMatch(/graph TB/);
|
|
186
|
+
// Confirm built-in renders raw by default too.
|
|
187
|
+
expect(stdout.text).not.toMatch(/```mermaid/);
|
|
164
188
|
});
|
|
165
189
|
it("resolves 'implement' without calling loadWorkflowFile", () => {
|
|
166
190
|
const stdout = captureStream();
|
|
@@ -158,6 +158,17 @@ describe("runUpgrade — no-op paths", () => {
|
|
|
158
158
|
expect(runInstall).toHaveBeenCalledWith("npm", ["install", "-g", "@sweny-ai/core@latest"]);
|
|
159
159
|
expect(exit).toHaveBeenCalledWith(0);
|
|
160
160
|
});
|
|
161
|
+
it("downgrades when --force is set against a local-dev build ahead of the registry", async () => {
|
|
162
|
+
const { deps, runInstall, exit } = makeDeps({
|
|
163
|
+
currentVersion: "0.2.0",
|
|
164
|
+
fetchLatestVersion: vi.fn(async () => "0.1.66"),
|
|
165
|
+
});
|
|
166
|
+
await runUpgrade({ force: true }, deps);
|
|
167
|
+
// --force explicitly overrides the "ahead" short-circuit — this is how
|
|
168
|
+
// someone pins back to the published train from a stale local build.
|
|
169
|
+
expect(runInstall).toHaveBeenCalledWith("npm", ["install", "-g", "@sweny-ai/core@latest"]);
|
|
170
|
+
expect(exit).toHaveBeenCalledWith(0);
|
|
171
|
+
});
|
|
161
172
|
});
|
|
162
173
|
describe("runUpgrade — install dispatch", () => {
|
|
163
174
|
it("invokes the detected package manager and exits 0 on success", async () => {
|
|
@@ -166,6 +166,23 @@ describe("maybeNudge", () => {
|
|
|
166
166
|
expect(readCache(cachePath)).toBeNull(); // fetch failed, nothing written
|
|
167
167
|
expect(stderr.text).toBe(""); // silent on error
|
|
168
168
|
});
|
|
169
|
+
it("gives up silently when the registry is slower than the budget", async () => {
|
|
170
|
+
// The race between fetch and the internal timer means a slow fetch should
|
|
171
|
+
// resolve to null → no cache write → no nudge. We pin down the behavior
|
|
172
|
+
// here so a future regression (forgetting the race or the timeout) gets
|
|
173
|
+
// caught even though the budget is configured in the module itself.
|
|
174
|
+
const slowFetch = vi.fn(() => new Promise((resolve) => {
|
|
175
|
+
// Never resolves within the test — the internal timer is what wins.
|
|
176
|
+
const t = setTimeout(() => resolve("9.9.9"), 10_000);
|
|
177
|
+
if (typeof t.unref === "function") {
|
|
178
|
+
t.unref();
|
|
179
|
+
}
|
|
180
|
+
}));
|
|
181
|
+
await maybeNudge(baseDeps({ fetchLatestVersion: slowFetch }));
|
|
182
|
+
expect(slowFetch).toHaveBeenCalled();
|
|
183
|
+
expect(readCache(cachePath)).toBeNull();
|
|
184
|
+
expect(stderr.text).toBe("");
|
|
185
|
+
});
|
|
169
186
|
it("does not refresh when cache is fresh (saves bandwidth)", async () => {
|
|
170
187
|
writeCache(cachePath, { latest: "0.1.65", checkedAt: 1_000_000 });
|
|
171
188
|
await maybeNudge(baseDeps({ now: 1_000_000 + 1 }));
|
package/dist/cli/check.js
CHANGED
|
@@ -29,21 +29,24 @@ export async function checkProviderConnectivity(config) {
|
|
|
29
29
|
});
|
|
30
30
|
}
|
|
31
31
|
// ── Observability ────────────────────────────────────────────────────────────
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
32
|
+
for (const provider of config.observabilityProviders) {
|
|
33
|
+
const creds = config.observabilityCredentials[provider] ?? {};
|
|
34
|
+
if (provider === "datadog") {
|
|
35
|
+
results.push(await checkDatadog(creds));
|
|
36
|
+
}
|
|
37
|
+
else if (provider === "sentry") {
|
|
38
|
+
results.push(await checkSentry(creds));
|
|
39
|
+
}
|
|
40
|
+
else if (provider === "file") {
|
|
41
|
+
results.push({ name: "Observability (file)", status: "skip", detail: "File provider — no network check needed" });
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
results.push({
|
|
45
|
+
name: `Observability (${provider})`,
|
|
46
|
+
status: "skip",
|
|
47
|
+
detail: "Connectivity check not implemented for this provider",
|
|
48
|
+
});
|
|
49
|
+
}
|
|
47
50
|
}
|
|
48
51
|
// ── Issue tracker ────────────────────────────────────────────────────────────
|
|
49
52
|
if (config.issueTrackerProvider === "linear") {
|
package/dist/cli/config.d.ts
CHANGED
|
@@ -7,8 +7,8 @@ export interface CliConfig {
|
|
|
7
7
|
claudeOauthToken: string;
|
|
8
8
|
openaiApiKey: string;
|
|
9
9
|
geminiApiKey: string;
|
|
10
|
-
|
|
11
|
-
observabilityCredentials: Record<string, string
|
|
10
|
+
observabilityProviders: string[];
|
|
11
|
+
observabilityCredentials: Record<string, Record<string, string>>;
|
|
12
12
|
issueTrackerProvider: string;
|
|
13
13
|
linearApiKey: string;
|
|
14
14
|
linearTeamId: string;
|
package/dist/cli/config.js
CHANGED
|
@@ -86,7 +86,15 @@ export function parseCliInputs(options, fileConfig = {}) {
|
|
|
86
86
|
return [fileVal];
|
|
87
87
|
return [];
|
|
88
88
|
};
|
|
89
|
-
const
|
|
89
|
+
const obsRaw = options.observabilityProvider || f("observability-provider") || "none";
|
|
90
|
+
const obsProviders = obsRaw
|
|
91
|
+
.split(",")
|
|
92
|
+
.map((s) => s.trim())
|
|
93
|
+
.filter((s) => s && s !== "none");
|
|
94
|
+
const obsCreds = {};
|
|
95
|
+
for (const p of obsProviders) {
|
|
96
|
+
obsCreds[p] = parseObservabilityCredentials(p, options, fileConfig);
|
|
97
|
+
}
|
|
90
98
|
return {
|
|
91
99
|
codingAgentProvider: options.agent || options.codingAgentProvider || f("coding-agent-provider") || "claude",
|
|
92
100
|
// Secrets: env only — never from config file
|
|
@@ -94,8 +102,8 @@ export function parseCliInputs(options, fileConfig = {}) {
|
|
|
94
102
|
claudeOauthToken: env.CLAUDE_CODE_OAUTH_TOKEN || "",
|
|
95
103
|
openaiApiKey: env.OPENAI_API_KEY || "",
|
|
96
104
|
geminiApiKey: env.GEMINI_API_KEY || env.GOOGLE_API_KEY || "",
|
|
97
|
-
|
|
98
|
-
observabilityCredentials:
|
|
105
|
+
observabilityProviders: obsProviders,
|
|
106
|
+
observabilityCredentials: obsCreds,
|
|
99
107
|
issueTrackerProvider: options.issueTrackerProvider || f("issue-tracker-provider") || "github-issues",
|
|
100
108
|
linearApiKey: env.LINEAR_API_KEY || "",
|
|
101
109
|
linearTeamId: options.linearTeamId || env.LINEAR_TEAM_ID || f("linear-team-id") || "",
|
|
@@ -218,122 +226,123 @@ export function validateInputs(config) {
|
|
|
218
226
|
if (!config.repository && !allLocal) {
|
|
219
227
|
errors.push("Missing: --repository <owner/repo> or GITHUB_REPOSITORY (could not auto-detect from git remote)");
|
|
220
228
|
}
|
|
221
|
-
// Observability credentials by provider
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
229
|
+
// Observability credentials by provider — validate each configured provider
|
|
230
|
+
for (const provider of config.observabilityProviders) {
|
|
231
|
+
const creds = config.observabilityCredentials[provider] ?? {};
|
|
232
|
+
switch (provider) {
|
|
233
|
+
case "datadog":
|
|
234
|
+
if (!creds.apiKey)
|
|
235
|
+
errors.push("Missing: DD_API_KEY — find API keys at https://app.datadoghq.com/organization-settings/api-keys");
|
|
236
|
+
if (!creds.appKey)
|
|
237
|
+
errors.push("Missing: DD_APP_KEY — find application keys at https://app.datadoghq.com/organization-settings/application-keys");
|
|
238
|
+
break;
|
|
239
|
+
case "sentry":
|
|
240
|
+
if (!creds.authToken)
|
|
241
|
+
errors.push("Missing: SENTRY_AUTH_TOKEN — create a token at https://sentry.io/settings/auth-tokens/");
|
|
242
|
+
if (!creds.organization)
|
|
243
|
+
errors.push("Missing: --sentry-org is required for sentry provider");
|
|
244
|
+
if (!creds.project)
|
|
245
|
+
errors.push("Missing: --sentry-project is required for sentry provider");
|
|
246
|
+
break;
|
|
247
|
+
case "cloudwatch":
|
|
248
|
+
if (!creds.logGroupPrefix)
|
|
249
|
+
errors.push("Missing: --cloudwatch-log-group-prefix is required for cloudwatch provider");
|
|
250
|
+
break;
|
|
251
|
+
case "splunk":
|
|
252
|
+
if (!creds.baseUrl)
|
|
253
|
+
errors.push("Missing: SPLUNK_URL is required for splunk provider");
|
|
254
|
+
if (!creds.token)
|
|
255
|
+
errors.push("Missing: SPLUNK_TOKEN is required for splunk provider");
|
|
256
|
+
break;
|
|
257
|
+
case "elastic":
|
|
258
|
+
if (!creds.baseUrl)
|
|
259
|
+
errors.push("Missing: ELASTIC_URL is required for elastic provider");
|
|
260
|
+
if (!creds.apiKey)
|
|
261
|
+
errors.push("Missing: ELASTIC_API_KEY is required for elastic provider");
|
|
262
|
+
break;
|
|
263
|
+
case "newrelic":
|
|
264
|
+
if (!creds.apiKey)
|
|
265
|
+
errors.push("Missing: NR_API_KEY is required for newrelic provider");
|
|
266
|
+
if (!creds.accountId)
|
|
267
|
+
errors.push("Missing: NR_ACCOUNT_ID is required for newrelic provider");
|
|
268
|
+
break;
|
|
269
|
+
case "loki":
|
|
270
|
+
if (!creds.baseUrl)
|
|
271
|
+
errors.push("Missing: LOKI_URL is required for loki provider");
|
|
272
|
+
break;
|
|
273
|
+
case "file":
|
|
274
|
+
if (!creds.path)
|
|
275
|
+
errors.push("Missing: --log-file <path> is required for file provider");
|
|
276
|
+
break;
|
|
277
|
+
case "prometheus":
|
|
278
|
+
if (!creds.url)
|
|
279
|
+
errors.push("Missing: PROMETHEUS_URL or --prometheus-url is required for prometheus provider");
|
|
280
|
+
break;
|
|
281
|
+
case "pagerduty":
|
|
282
|
+
if (!creds.apiKey)
|
|
283
|
+
errors.push("Missing: PAGERDUTY_API_KEY is required for pagerduty provider");
|
|
284
|
+
break;
|
|
285
|
+
case "heroku":
|
|
286
|
+
if (!creds.apiKey)
|
|
287
|
+
errors.push("Missing: HEROKU_API_KEY is required for heroku provider");
|
|
288
|
+
if (!creds.appName)
|
|
289
|
+
errors.push("Missing: HEROKU_APP_NAME or --heroku-app-name is required for heroku provider");
|
|
290
|
+
break;
|
|
291
|
+
case "opsgenie":
|
|
292
|
+
if (!creds.apiKey)
|
|
293
|
+
errors.push("Missing: OPSGENIE_API_KEY is required for opsgenie provider");
|
|
294
|
+
break;
|
|
295
|
+
case "honeycomb":
|
|
296
|
+
if (!creds.apiKey)
|
|
297
|
+
errors.push("Missing: HONEYCOMB_API_KEY is required for honeycomb provider");
|
|
298
|
+
if (!creds.dataset)
|
|
299
|
+
errors.push("Missing: HONEYCOMB_DATASET is required for honeycomb provider");
|
|
300
|
+
break;
|
|
301
|
+
case "axiom":
|
|
302
|
+
if (!creds.apiToken)
|
|
303
|
+
errors.push("Missing: AXIOM_TOKEN is required for axiom provider");
|
|
304
|
+
if (!creds.dataset)
|
|
305
|
+
errors.push("Missing: AXIOM_DATASET or --axiom-dataset is required for axiom provider");
|
|
306
|
+
break;
|
|
307
|
+
case "betterstack":
|
|
308
|
+
if (!creds.apiToken)
|
|
309
|
+
errors.push("Missing: BETTERSTACK_API_TOKEN, BETTERSTACK_TELEMETRY_TOKEN, or BETTERSTACK_UPTIME_TOKEN is required for betterstack provider");
|
|
310
|
+
if (!creds.sourceId && !creds.tableName)
|
|
311
|
+
errors.push("Missing: either BETTERSTACK_SOURCE_ID (--betterstack-source-id) or BETTERSTACK_TABLE_NAME (--betterstack-table-name) is required for betterstack provider");
|
|
312
|
+
break;
|
|
313
|
+
case "vercel":
|
|
314
|
+
if (!creds.token)
|
|
315
|
+
errors.push("Missing: VERCEL_TOKEN — create a token at https://vercel.com/account/tokens");
|
|
316
|
+
if (!creds.projectId)
|
|
317
|
+
errors.push("Missing: VERCEL_PROJECT_ID — find it in your Vercel project settings");
|
|
318
|
+
break;
|
|
319
|
+
case "supabase":
|
|
320
|
+
if (!creds.managementApiKey)
|
|
321
|
+
errors.push("Missing: SUPABASE_MANAGEMENT_KEY — create a token at https://supabase.com/dashboard/account/tokens");
|
|
322
|
+
if (!creds.projectRef)
|
|
323
|
+
errors.push("Missing: SUPABASE_PROJECT_REF — find it in your Supabase project settings > General");
|
|
324
|
+
break;
|
|
325
|
+
case "netlify":
|
|
326
|
+
if (!creds.token)
|
|
327
|
+
errors.push("Missing: NETLIFY_TOKEN — create a token at https://app.netlify.com/user/applications#personal-access-tokens");
|
|
328
|
+
if (!creds.siteId)
|
|
329
|
+
errors.push("Missing: NETLIFY_SITE_ID — find it in Site Settings > General > Site details");
|
|
330
|
+
break;
|
|
331
|
+
case "fly":
|
|
332
|
+
if (!creds.token)
|
|
333
|
+
errors.push("Missing: FLY_TOKEN — create a token at https://fly.io/user/personal_access_tokens");
|
|
334
|
+
if (!creds.appName)
|
|
335
|
+
errors.push("Missing: FLY_APP_NAME — the name of your Fly.io application");
|
|
336
|
+
break;
|
|
337
|
+
case "render":
|
|
338
|
+
if (!creds.apiKey)
|
|
339
|
+
errors.push("Missing: RENDER_API_KEY — create an API key at https://dashboard.render.com/u/settings");
|
|
340
|
+
if (!creds.serviceId)
|
|
341
|
+
errors.push("Missing: RENDER_SERVICE_ID — find it in your service's Settings page (srv-...)");
|
|
342
|
+
break;
|
|
343
|
+
default:
|
|
344
|
+
errors.push(`Unknown --observability-provider "${provider}". Valid values: none, datadog, sentry, cloudwatch, splunk, elastic, newrelic, loki, prometheus, pagerduty, honeycomb, heroku, opsgenie, axiom, betterstack, vercel, supabase, netlify, fly, render, file`);
|
|
345
|
+
}
|
|
337
346
|
}
|
|
338
347
|
// Issue tracker credentials by provider
|
|
339
348
|
switch (config.issueTrackerProvider) {
|
package/dist/cli/config.test.js
CHANGED
|
@@ -12,8 +12,8 @@ function baseConfig(overrides = {}) {
|
|
|
12
12
|
claudeOauthToken: "",
|
|
13
13
|
openaiApiKey: "",
|
|
14
14
|
geminiApiKey: "",
|
|
15
|
-
|
|
16
|
-
observabilityCredentials: {},
|
|
15
|
+
observabilityProviders: ["file"],
|
|
16
|
+
observabilityCredentials: { file: {} },
|
|
17
17
|
issueTrackerProvider: "file",
|
|
18
18
|
linearApiKey: "",
|
|
19
19
|
linearTeamId: "",
|
|
@@ -114,23 +114,27 @@ describe("validateInputs — notification provider", () => {
|
|
|
114
114
|
});
|
|
115
115
|
describe("validateInputs — observability provider", () => {
|
|
116
116
|
it("accepts 'none' without requiring any credentials", () => {
|
|
117
|
-
const errors = validateInputs(baseConfig({
|
|
117
|
+
const errors = validateInputs(baseConfig({ observabilityProviders: [], observabilityCredentials: {} }));
|
|
118
118
|
expect(errors.filter((e) => e.includes("DD_") || e.includes("observability"))).toEqual([]);
|
|
119
119
|
});
|
|
120
120
|
it("requires DD keys when explicitly set to datadog", () => {
|
|
121
|
-
const errors = validateInputs(baseConfig({
|
|
121
|
+
const errors = validateInputs(baseConfig({ observabilityProviders: ["datadog"], observabilityCredentials: {} }));
|
|
122
122
|
expect(errors.some((e) => e.includes("DD_API_KEY"))).toBe(true);
|
|
123
123
|
expect(errors.some((e) => e.includes("DD_APP_KEY"))).toBe(true);
|
|
124
124
|
});
|
|
125
125
|
});
|
|
126
126
|
describe("parseCliInputs — observability provider default", () => {
|
|
127
|
-
it("defaults to
|
|
127
|
+
it("defaults to empty array when no provider is configured", () => {
|
|
128
128
|
const config = parseCliInputs({}, {});
|
|
129
|
-
expect(config.
|
|
129
|
+
expect(config.observabilityProviders).toEqual([]);
|
|
130
130
|
});
|
|
131
131
|
it("respects explicit datadog provider", () => {
|
|
132
132
|
const config = parseCliInputs({ observabilityProvider: "datadog" }, {});
|
|
133
|
-
expect(config.
|
|
133
|
+
expect(config.observabilityProviders).toEqual(["datadog"]);
|
|
134
|
+
});
|
|
135
|
+
it("supports comma-separated multiple providers", () => {
|
|
136
|
+
const config = parseCliInputs({ observabilityProvider: "loki,sentry" }, {});
|
|
137
|
+
expect(config.observabilityProviders).toEqual(["loki", "sentry"]);
|
|
134
138
|
});
|
|
135
139
|
});
|
|
136
140
|
describe("fetch.auth + offline parsing", () => {
|
package/dist/cli/diagram.d.ts
CHANGED
|
@@ -1,16 +1,24 @@
|
|
|
1
|
-
import type { Command } from "commander";
|
|
2
1
|
import type { Workflow } from "../types.js";
|
|
3
2
|
export interface DiagramOptions {
|
|
4
3
|
direction?: string;
|
|
4
|
+
/**
|
|
5
|
+
* Optional diagram title. When omitted we render pure Mermaid with no
|
|
6
|
+
* `---\ntitle: …\n---` frontmatter — the format that `mmdc`, Mermaid Live
|
|
7
|
+
* Editor, and `@mermaid-js/mermaid-cli` all accept as-is.
|
|
8
|
+
*/
|
|
5
9
|
title?: string;
|
|
10
|
+
/**
|
|
11
|
+
* Wrap output in a ```mermaid fenced code block. Tri-state:
|
|
12
|
+
* - `true` → fenced (forced by explicit `--block`)
|
|
13
|
+
* - `false` → raw (forced by explicit `--no-block`)
|
|
14
|
+
* - `undefined` → inferred from `--output` extension; default is raw
|
|
15
|
+
*/
|
|
6
16
|
block?: boolean;
|
|
7
17
|
output?: string;
|
|
8
18
|
}
|
|
9
19
|
export interface DiagramDeps {
|
|
10
20
|
/** Load a workflow file (YAML or JSON). Injected so tests can stub. */
|
|
11
21
|
loadWorkflowFile: (file: string) => Workflow;
|
|
12
|
-
/** Commander instance used to detect whether --block was set explicitly. */
|
|
13
|
-
command?: Command;
|
|
14
22
|
/** Stream for success/error messages. Defaults to process.stderr. */
|
|
15
23
|
stderr?: NodeJS.WritableStream;
|
|
16
24
|
/** Stream for diagram output when no --output is set. Defaults to process.stdout. */
|
|
@@ -18,13 +26,24 @@ export interface DiagramDeps {
|
|
|
18
26
|
/** Exit handler. Defaults to process.exit. Tests can throw instead. */
|
|
19
27
|
exit?: (code: number) => void;
|
|
20
28
|
}
|
|
29
|
+
/**
|
|
30
|
+
* Resolve whether output should be wrapped in a ```mermaid fenced block.
|
|
31
|
+
*
|
|
32
|
+
* Precedence:
|
|
33
|
+
* 1. Explicit `--block` / `--no-block` always wins.
|
|
34
|
+
* 2. When `-o` points at `.md`/`.markdown` we fence (markdown context).
|
|
35
|
+
* 3. Everything else — stdout, `.mmd`, `.mermaid`, unknown extensions —
|
|
36
|
+
* emits raw Mermaid syntax that Mermaid CLI / Live Editor / mmdc can
|
|
37
|
+
* consume directly with no preprocessing.
|
|
38
|
+
*/
|
|
39
|
+
export declare function resolveUseBlock(options: DiagramOptions): boolean;
|
|
21
40
|
/**
|
|
22
41
|
* Render a workflow as a Mermaid diagram and write to stdout or a file.
|
|
23
42
|
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
43
|
+
* By default the output is pure Mermaid — no ```mermaid fence, no
|
|
44
|
+
* `---\ntitle: …\n---` frontmatter — so the result is directly consumable
|
|
45
|
+
* by Mermaid CLI (`mmdc`) and the Live Editor. Pass `--block` to wrap in a
|
|
46
|
+
* fenced code block for README embedding, or `--title` to inject a title
|
|
47
|
+
* header for Mermaid renderers that support it.
|
|
29
48
|
*/
|
|
30
49
|
export declare function runWorkflowDiagram(file: string, options: DiagramOptions, deps: DiagramDeps): void;
|
package/dist/cli/diagram.js
CHANGED
|
@@ -3,14 +3,37 @@ import * as path from "node:path";
|
|
|
3
3
|
import chalk from "chalk";
|
|
4
4
|
import { toMermaid, toMermaidBlock } from "../mermaid.js";
|
|
5
5
|
import { triageWorkflow, implementWorkflow, seedContentWorkflow } from "../workflows/index.js";
|
|
6
|
+
const MARKDOWN_EXTS = new Set([".md", ".markdown"]);
|
|
7
|
+
/**
|
|
8
|
+
* Resolve whether output should be wrapped in a ```mermaid fenced block.
|
|
9
|
+
*
|
|
10
|
+
* Precedence:
|
|
11
|
+
* 1. Explicit `--block` / `--no-block` always wins.
|
|
12
|
+
* 2. When `-o` points at `.md`/`.markdown` we fence (markdown context).
|
|
13
|
+
* 3. Everything else — stdout, `.mmd`, `.mermaid`, unknown extensions —
|
|
14
|
+
* emits raw Mermaid syntax that Mermaid CLI / Live Editor / mmdc can
|
|
15
|
+
* consume directly with no preprocessing.
|
|
16
|
+
*/
|
|
17
|
+
export function resolveUseBlock(options) {
|
|
18
|
+
if (options.block === true)
|
|
19
|
+
return true;
|
|
20
|
+
if (options.block === false)
|
|
21
|
+
return false;
|
|
22
|
+
if (options.output) {
|
|
23
|
+
const ext = path.extname(options.output).toLowerCase();
|
|
24
|
+
if (MARKDOWN_EXTS.has(ext))
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
6
29
|
/**
|
|
7
30
|
* Render a workflow as a Mermaid diagram and write to stdout or a file.
|
|
8
31
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
32
|
+
* By default the output is pure Mermaid — no ```mermaid fence, no
|
|
33
|
+
* `---\ntitle: …\n---` frontmatter — so the result is directly consumable
|
|
34
|
+
* by Mermaid CLI (`mmdc`) and the Live Editor. Pass `--block` to wrap in a
|
|
35
|
+
* fenced code block for README embedding, or `--title` to inject a title
|
|
36
|
+
* header for Mermaid renderers that support it.
|
|
14
37
|
*/
|
|
15
38
|
export function runWorkflowDiagram(file, options, deps) {
|
|
16
39
|
const stderr = deps.stderr ?? process.stderr;
|
|
@@ -38,21 +61,14 @@ export function runWorkflowDiagram(file, options, deps) {
|
|
|
38
61
|
}
|
|
39
62
|
}
|
|
40
63
|
const direction = (options.direction === "LR" ? "LR" : "TB");
|
|
41
|
-
const
|
|
42
|
-
// Explicit --block / --no-block always wins. When commander's source is
|
|
43
|
-
// "cli" or "env" the user (or env var) set it; otherwise we may infer.
|
|
44
|
-
const blockSource = deps.command?.getOptionValueSource("block");
|
|
45
|
-
const blockExplicit = blockSource === "cli" || blockSource === "env";
|
|
46
|
-
let useBlock = options.block !== false;
|
|
47
|
-
if (!blockExplicit && options.output) {
|
|
48
|
-
const ext = path.extname(options.output).toLowerCase();
|
|
49
|
-
if (ext === ".mmd")
|
|
50
|
-
useBlock = false;
|
|
51
|
-
else if (ext === ".md")
|
|
52
|
-
useBlock = true;
|
|
53
|
-
}
|
|
64
|
+
const useBlock = resolveUseBlock(options);
|
|
54
65
|
const render = useBlock ? toMermaidBlock : toMermaid;
|
|
55
|
-
|
|
66
|
+
// Title is opt-in only — an unset `--title` produces no frontmatter. Users
|
|
67
|
+
// who want the workflow.name as title can pass `--title "$(name)"`.
|
|
68
|
+
const renderOpts = { direction };
|
|
69
|
+
if (options.title)
|
|
70
|
+
renderOpts.title = options.title;
|
|
71
|
+
const output = render(workflow, renderOpts) + "\n";
|
|
56
72
|
if (options.output) {
|
|
57
73
|
try {
|
|
58
74
|
// Create parent dirs so `-o some/nested/diagram.mmd` works without a
|
package/dist/cli/main.js
CHANGED
|
@@ -111,7 +111,7 @@ function buildMcpAutoConfig(config) {
|
|
|
111
111
|
return {
|
|
112
112
|
sourceControlProvider: config.sourceControlProvider,
|
|
113
113
|
issueTrackerProvider: config.issueTrackerProvider,
|
|
114
|
-
|
|
114
|
+
observabilityProviders: config.observabilityProviders,
|
|
115
115
|
credentials: buildCredentialMap(),
|
|
116
116
|
workspaceTools: config.workspaceTools,
|
|
117
117
|
userMcpServers: Object.keys(config.mcpServers).length > 0 ? config.mcpServers : undefined,
|
|
@@ -122,14 +122,15 @@ function buildMcpAutoConfig(config) {
|
|
|
122
122
|
*/
|
|
123
123
|
function buildProviderCtx(config, mcpServers) {
|
|
124
124
|
const extras = {};
|
|
125
|
-
|
|
126
|
-
|
|
125
|
+
const bsCreds = config.observabilityCredentials["betterstack"];
|
|
126
|
+
if (bsCreds?.sourceId) {
|
|
127
|
+
extras["BetterStack source ID"] = bsCreds.sourceId;
|
|
127
128
|
}
|
|
128
|
-
if (
|
|
129
|
-
extras["BetterStack table name"] =
|
|
129
|
+
if (bsCreds?.tableName) {
|
|
130
|
+
extras["BetterStack table name"] = bsCreds.tableName;
|
|
130
131
|
}
|
|
131
132
|
return buildProviderContext({
|
|
132
|
-
|
|
133
|
+
observabilityProviders: config.observabilityProviders,
|
|
133
134
|
issueTrackerProvider: config.issueTrackerProvider,
|
|
134
135
|
sourceControlProvider: config.sourceControlProvider,
|
|
135
136
|
mcpServers: Object.keys(mcpServers),
|
|
@@ -306,6 +307,7 @@ triageCmd.action(async (options) => {
|
|
|
306
307
|
if (config.additionalInstructions)
|
|
307
308
|
contextParts.push(config.additionalInstructions);
|
|
308
309
|
const fullContext = [contextParts.join("\n\n"), context].filter(Boolean).join("\n\n---\n\n");
|
|
310
|
+
const bsCreds = config.observabilityCredentials["betterstack"];
|
|
309
311
|
const workflowInput = {
|
|
310
312
|
timeRange: config.timeRange,
|
|
311
313
|
severityFocus: config.severityFocus,
|
|
@@ -320,12 +322,12 @@ triageCmd.action(async (options) => {
|
|
|
320
322
|
issueOverride: config.issueOverride,
|
|
321
323
|
noveltyMode: config.noveltyMode,
|
|
322
324
|
reviewMode: config.reviewMode,
|
|
323
|
-
|
|
324
|
-
...(
|
|
325
|
-
betterstackSourceId:
|
|
325
|
+
observabilityProviders: config.observabilityProviders,
|
|
326
|
+
...(bsCreds?.sourceId && {
|
|
327
|
+
betterstackSourceId: bsCreds.sourceId,
|
|
326
328
|
}),
|
|
327
|
-
...(
|
|
328
|
-
betterstackTableName:
|
|
329
|
+
...(bsCreds?.tableName && {
|
|
330
|
+
betterstackTableName: bsCreds.tableName,
|
|
329
331
|
}),
|
|
330
332
|
// Structured rules/context for executor (URLs resolved eagerly by loadAdditionalContext)
|
|
331
333
|
rules,
|
|
@@ -665,12 +667,12 @@ export async function workflowRunAction(file, options) {
|
|
|
665
667
|
baseBranch: config.baseBranch,
|
|
666
668
|
prLabels: config.prLabels,
|
|
667
669
|
additionalInstructions: config.additionalInstructions,
|
|
668
|
-
|
|
669
|
-
...(config.observabilityCredentials
|
|
670
|
-
betterstackSourceId: config.observabilityCredentials.sourceId,
|
|
670
|
+
observabilityProviders: config.observabilityProviders,
|
|
671
|
+
...(config.observabilityCredentials["betterstack"]?.sourceId && {
|
|
672
|
+
betterstackSourceId: config.observabilityCredentials["betterstack"].sourceId,
|
|
671
673
|
}),
|
|
672
|
-
...(config.observabilityCredentials
|
|
673
|
-
betterstackTableName: config.observabilityCredentials.tableName,
|
|
674
|
+
...(config.observabilityCredentials["betterstack"]?.tableName && {
|
|
675
|
+
betterstackTableName: config.observabilityCredentials["betterstack"].tableName,
|
|
674
676
|
}),
|
|
675
677
|
context: buildProviderCtx(config, mcpServers),
|
|
676
678
|
};
|
|
@@ -799,14 +801,14 @@ workflowCmd
|
|
|
799
801
|
.action(workflowRunAction);
|
|
800
802
|
workflowCmd
|
|
801
803
|
.command("diagram <file>")
|
|
802
|
-
.description("Render a workflow as a Mermaid diagram")
|
|
804
|
+
.description("Render a workflow as a Mermaid diagram (raw .mmd by default; .md output auto-fences)")
|
|
803
805
|
.option("--direction <dir>", "Graph direction: TB (top-bottom) or LR (left-right)", "TB")
|
|
804
|
-
.option("--title <title>", "
|
|
805
|
-
.option("--block", "Wrap in ```mermaid fenced code block (
|
|
806
|
-
.option("--no-block", "
|
|
807
|
-
.option("-o, --output <path>", "Write
|
|
808
|
-
.action(
|
|
809
|
-
runWorkflowDiagram(file, options, { loadWorkflowFile
|
|
806
|
+
.option("--title <title>", "Inject a title header (off by default — raw Mermaid has no title)")
|
|
807
|
+
.option("--block", "Wrap in ```mermaid fenced code block (forces fencing in any output)")
|
|
808
|
+
.option("--no-block", "Force raw Mermaid even when writing to a .md file")
|
|
809
|
+
.option("-o, --output <path>", "Write to a file instead of stdout (.mmd/.mermaid raw; .md fenced)")
|
|
810
|
+
.action((file, options) => {
|
|
811
|
+
runWorkflowDiagram(file, options, { loadWorkflowFile });
|
|
810
812
|
});
|
|
811
813
|
workflowCmd
|
|
812
814
|
.command("export <name>")
|
|
@@ -967,7 +969,7 @@ workflowCmd
|
|
|
967
969
|
// ── sweny upgrade / update ────────────────────────────────────────────
|
|
968
970
|
// Self-update the globally-installed @sweny-ai/core. Mirrors the UX of
|
|
969
971
|
// `bun upgrade`, `deno upgrade`, `rustup update`, etc.
|
|
970
|
-
|
|
972
|
+
program
|
|
971
973
|
.command("upgrade")
|
|
972
974
|
.alias("update")
|
|
973
975
|
.description("Upgrade sweny to the latest published version")
|
|
@@ -991,7 +993,13 @@ const upgradeCmd = program
|
|
|
991
993
|
installPath,
|
|
992
994
|
fetchLatestVersion: fetchLatestFromNpm,
|
|
993
995
|
runInstall: (cmd, args) => {
|
|
994
|
-
|
|
996
|
+
// Windows: `npm`/`pnpm`/`yarn` are `.cmd` shims and Node's spawn can't
|
|
997
|
+
// resolve them without a shell. Everywhere else we skip the shell to
|
|
998
|
+
// keep argv unambiguous and avoid injection surface.
|
|
999
|
+
const res = spawnSync(cmd, args, {
|
|
1000
|
+
stdio: "inherit",
|
|
1001
|
+
shell: process.platform === "win32",
|
|
1002
|
+
});
|
|
995
1003
|
if (res.error) {
|
|
996
1004
|
process.stderr.write(chalk.red(` Error: couldn't run ${cmd} — ${res.error.message}`) + "\n");
|
|
997
1005
|
return 127;
|
|
@@ -1009,8 +1017,6 @@ const upgradeCmd = program
|
|
|
1009
1017
|
},
|
|
1010
1018
|
});
|
|
1011
1019
|
});
|
|
1012
|
-
// Touch upgradeCmd so TS/ESLint don't flag it unused if someone tightens rules.
|
|
1013
|
-
void upgradeCmd;
|
|
1014
1020
|
// ── Passive "new version available" nudge ─────────────────────────────
|
|
1015
1021
|
// Runs after every command. Non-blocking, bounded to 1.5s, silent on any
|
|
1016
1022
|
// failure, skipped in CI / non-TTY / opt-out environments.
|
package/dist/cli/output.js
CHANGED
|
@@ -89,7 +89,7 @@ export function formatBanner(config, version) {
|
|
|
89
89
|
const rows = [
|
|
90
90
|
`${c.subtle("Repository")}${" ".repeat(6)}${chalk.white(config.repository)}`,
|
|
91
91
|
`${c.subtle("Agent")}${" ".repeat(11)}${chalk.white(config.codingAgentProvider)}`,
|
|
92
|
-
`${c.subtle("Observability")}${" ".repeat(3)}${chalk.white(config.
|
|
92
|
+
`${c.subtle("Observability")}${" ".repeat(3)}${chalk.white(config.observabilityProviders.join(", "))}`,
|
|
93
93
|
`${c.subtle("Issue tracker")}${" ".repeat(3)}${chalk.white(config.issueTrackerProvider)}`,
|
|
94
94
|
`${c.subtle("Time range")}${" ".repeat(6)}${chalk.white(config.timeRange)}`,
|
|
95
95
|
`${c.subtle("Mode")}${" ".repeat(12)}${mode}`,
|
|
@@ -460,8 +460,8 @@ export function formatDagResultMarkdown(results, durationMs, config, options = {
|
|
|
460
460
|
const rows = [];
|
|
461
461
|
if (config?.repository)
|
|
462
462
|
rows.push(["Repository", `\`${config.repository}\``]);
|
|
463
|
-
if (config?.
|
|
464
|
-
rows.push(["Observability", config.
|
|
463
|
+
if (config?.observabilityProviders?.length)
|
|
464
|
+
rows.push(["Observability", config.observabilityProviders.join(", ")]);
|
|
465
465
|
if (config?.issueTrackerProvider)
|
|
466
466
|
rows.push(["Issue tracker", config.issueTrackerProvider]);
|
|
467
467
|
if (config?.timeRange)
|
package/dist/cli/output.test.js
CHANGED
|
@@ -12,7 +12,7 @@ function minimalConfig(overrides = {}) {
|
|
|
12
12
|
timeRange: "6h",
|
|
13
13
|
serviceFilter: "permit-service",
|
|
14
14
|
repository: "acme/app",
|
|
15
|
-
|
|
15
|
+
observabilityProviders: ["betterstack"],
|
|
16
16
|
issueTrackerProvider: "linear",
|
|
17
17
|
dryRun: false,
|
|
18
18
|
...overrides,
|
package/dist/mcp.d.ts
CHANGED
|
@@ -35,7 +35,7 @@ export declare function buildSkillMcpServers(opts: SkillMcpOptions): Record<stri
|
|
|
35
35
|
*/
|
|
36
36
|
export declare function buildAutoMcpServers(config: McpAutoConfig): Record<string, McpServerConfig>;
|
|
37
37
|
export interface ProviderContextOptions {
|
|
38
|
-
|
|
38
|
+
observabilityProviders?: string[];
|
|
39
39
|
issueTrackerProvider?: string;
|
|
40
40
|
sourceControlProvider?: string;
|
|
41
41
|
/** Which MCP servers were actually injected (keys from buildAutoMcpServers) */
|
package/dist/mcp.js
CHANGED
|
@@ -230,10 +230,14 @@ export function buildAutoMcpServers(config) {
|
|
|
230
230
|
},
|
|
231
231
|
};
|
|
232
232
|
}
|
|
233
|
+
// ── Observability MCP servers ──────────────────────────────────
|
|
234
|
+
// Inject MCP for every configured observability provider whose credentials
|
|
235
|
+
// are present. Multiple providers are supported (e.g. "loki,sentry").
|
|
236
|
+
const obsProviders = new Set(config.observabilityProviders ?? []);
|
|
233
237
|
// Datadog MCP — HTTP transport.
|
|
234
238
|
const ddApiKey = creds.DD_API_KEY;
|
|
235
239
|
const ddAppKey = creds.DD_APP_KEY;
|
|
236
|
-
if (
|
|
240
|
+
if (obsProviders.has("datadog") && ddApiKey && ddAppKey) {
|
|
237
241
|
auto["datadog"] = {
|
|
238
242
|
type: "http",
|
|
239
243
|
url: "https://mcp.datadoghq.com/api/unstable/mcp-server/mcp",
|
|
@@ -242,7 +246,7 @@ export function buildAutoMcpServers(config) {
|
|
|
242
246
|
}
|
|
243
247
|
// Sentry MCP — @sentry/mcp-server reads SENTRY_ACCESS_TOKEN (not SENTRY_AUTH_TOKEN).
|
|
244
248
|
const sentryAuthToken = creds.SENTRY_AUTH_TOKEN;
|
|
245
|
-
if (
|
|
249
|
+
if (obsProviders.has("sentry") && sentryAuthToken) {
|
|
246
250
|
const sentryEnv = { SENTRY_ACCESS_TOKEN: sentryAuthToken };
|
|
247
251
|
const sentryUrl = creds.SENTRY_URL;
|
|
248
252
|
if (sentryUrl && sentryUrl !== "https://sentry.io") {
|
|
@@ -261,10 +265,8 @@ export function buildAutoMcpServers(config) {
|
|
|
261
265
|
};
|
|
262
266
|
}
|
|
263
267
|
// New Relic MCP — HTTP transport; region-aware endpoint.
|
|
264
|
-
// Header key is "Api-Key" (not "Authorization"), unique to New Relic's MCP.
|
|
265
|
-
// Trailing slash is intentional — New Relic's MCP spec requires it.
|
|
266
268
|
const nrApiKey = creds.NEW_RELIC_API_KEY;
|
|
267
|
-
if (
|
|
269
|
+
if (obsProviders.has("newrelic") && nrApiKey) {
|
|
268
270
|
const nrRegion = creds.NEW_RELIC_REGION;
|
|
269
271
|
const nrEndpoint = nrRegion === "eu" ? "https://mcp.eu.newrelic.com/mcp/" : "https://mcp.newrelic.com/mcp/";
|
|
270
272
|
auto["newrelic"] = {
|
|
@@ -274,13 +276,8 @@ export function buildAutoMcpServers(config) {
|
|
|
274
276
|
};
|
|
275
277
|
}
|
|
276
278
|
// Better Stack MCP — HTTP remote MCP; Bearer token auth.
|
|
277
|
-
// BetterStack uses separate tokens for Uptime vs Telemetry APIs.
|
|
278
|
-
// The MCP server accepts the telemetry token for log/metric queries.
|
|
279
|
-
// Accept: BETTERSTACK_API_TOKEN (legacy), BETTERSTACK_TELEMETRY_TOKEN, or BETTERSTACK_UPTIME_TOKEN.
|
|
280
|
-
// Injected whenever any token is present (not just when it's the primary provider)
|
|
281
|
-
// because BetterStack logs complement any primary observability provider.
|
|
282
279
|
const bsApiToken = creds.BETTERSTACK_API_TOKEN || creds.BETTERSTACK_TELEMETRY_TOKEN || creds.BETTERSTACK_UPTIME_TOKEN;
|
|
283
|
-
if (bsApiToken) {
|
|
280
|
+
if (obsProviders.has("betterstack") && bsApiToken) {
|
|
284
281
|
auto["betterstack"] = {
|
|
285
282
|
type: "http",
|
|
286
283
|
url: "https://mcp.betterstack.com",
|
|
@@ -361,16 +358,13 @@ export function buildAutoMcpServers(config) {
|
|
|
361
358
|
*/
|
|
362
359
|
export function buildProviderContext(opts) {
|
|
363
360
|
const lines = ["## Available Providers & Tools", ""];
|
|
364
|
-
// Observability
|
|
365
|
-
|
|
366
|
-
|
|
361
|
+
// Observability — list all configured providers
|
|
362
|
+
const obsProviders = opts.observabilityProviders ?? [];
|
|
363
|
+
for (const provider of obsProviders) {
|
|
364
|
+
const mcpNote = opts.mcpServers.includes(provider)
|
|
367
365
|
? ` (available via MCP — use its tools to query logs, errors, and metrics)`
|
|
368
366
|
: "";
|
|
369
|
-
lines.push(`- **Observability**: ${
|
|
370
|
-
}
|
|
371
|
-
// BetterStack as secondary (token present but not primary provider)
|
|
372
|
-
if (opts.observabilityProvider !== "betterstack" && opts.mcpServers.includes("betterstack")) {
|
|
373
|
-
lines.push(`- **Logs**: betterstack (available via MCP — use its tools to query logs)`);
|
|
367
|
+
lines.push(`- **Observability**: ${provider}${mcpNote}`);
|
|
374
368
|
}
|
|
375
369
|
// Issue tracker
|
|
376
370
|
if (opts.issueTrackerProvider) {
|
package/dist/mcp.test.js
CHANGED
|
@@ -47,7 +47,7 @@ describe("buildAutoMcpServers", () => {
|
|
|
47
47
|
// ── Datadog MCP ─────────────────────────────────────────────────
|
|
48
48
|
it("injects Datadog MCP (HTTP) when observabilityProvider is datadog with DD_API_KEY + DD_APP_KEY", () => {
|
|
49
49
|
const result = buildAutoMcpServers(cfg({
|
|
50
|
-
|
|
50
|
+
observabilityProviders: ["datadog"],
|
|
51
51
|
credentials: { DD_API_KEY: "dd_api", DD_APP_KEY: "dd_app" },
|
|
52
52
|
}));
|
|
53
53
|
expect(result["datadog"]).toEqual({
|
|
@@ -57,12 +57,12 @@ describe("buildAutoMcpServers", () => {
|
|
|
57
57
|
});
|
|
58
58
|
});
|
|
59
59
|
it("does NOT inject Datadog MCP when DD_APP_KEY is missing", () => {
|
|
60
|
-
const result = buildAutoMcpServers(cfg({
|
|
60
|
+
const result = buildAutoMcpServers(cfg({ observabilityProviders: ["datadog"], credentials: { DD_API_KEY: "dd_api" } }));
|
|
61
61
|
expect(result["datadog"]).toBeUndefined();
|
|
62
62
|
});
|
|
63
63
|
// ── Sentry MCP ──────────────────────────────────────────────────
|
|
64
64
|
it("injects Sentry MCP (stdio) when observabilityProvider is sentry with SENTRY_AUTH_TOKEN", () => {
|
|
65
|
-
const result = buildAutoMcpServers(cfg({
|
|
65
|
+
const result = buildAutoMcpServers(cfg({ observabilityProviders: ["sentry"], credentials: { SENTRY_AUTH_TOKEN: "sntryu_abc" } }));
|
|
66
66
|
expect(result["sentry"]).toEqual({
|
|
67
67
|
type: "stdio",
|
|
68
68
|
command: "npx",
|
|
@@ -72,7 +72,7 @@ describe("buildAutoMcpServers", () => {
|
|
|
72
72
|
});
|
|
73
73
|
it("injects Sentry MCP with self-hosted SENTRY_HOST from SENTRY_URL", () => {
|
|
74
74
|
const result = buildAutoMcpServers(cfg({
|
|
75
|
-
|
|
75
|
+
observabilityProviders: ["sentry"],
|
|
76
76
|
credentials: {
|
|
77
77
|
SENTRY_AUTH_TOKEN: "sntryu_abc",
|
|
78
78
|
SENTRY_URL: "https://sentry.mycompany.com",
|
|
@@ -85,7 +85,7 @@ describe("buildAutoMcpServers", () => {
|
|
|
85
85
|
});
|
|
86
86
|
it("does NOT set SENTRY_HOST when SENTRY_URL is sentry.io", () => {
|
|
87
87
|
const result = buildAutoMcpServers(cfg({
|
|
88
|
-
|
|
88
|
+
observabilityProviders: ["sentry"],
|
|
89
89
|
credentials: {
|
|
90
90
|
SENTRY_AUTH_TOKEN: "sntryu_abc",
|
|
91
91
|
SENTRY_URL: "https://sentry.io",
|
|
@@ -95,7 +95,7 @@ describe("buildAutoMcpServers", () => {
|
|
|
95
95
|
});
|
|
96
96
|
it("handles malformed SENTRY_URL gracefully (no SENTRY_HOST set)", () => {
|
|
97
97
|
const result = buildAutoMcpServers(cfg({
|
|
98
|
-
|
|
98
|
+
observabilityProviders: ["sentry"],
|
|
99
99
|
credentials: {
|
|
100
100
|
SENTRY_AUTH_TOKEN: "sntryu_abc",
|
|
101
101
|
SENTRY_URL: "not-a-url",
|
|
@@ -143,7 +143,7 @@ describe("buildAutoMcpServers", () => {
|
|
|
143
143
|
// ── New Relic MCP ───────────────────────────────────────────────
|
|
144
144
|
it("injects New Relic MCP with EU region endpoint", () => {
|
|
145
145
|
const result = buildAutoMcpServers(cfg({
|
|
146
|
-
|
|
146
|
+
observabilityProviders: ["newrelic"],
|
|
147
147
|
credentials: { NEW_RELIC_API_KEY: "NRAK-abc", NEW_RELIC_REGION: "eu" },
|
|
148
148
|
}));
|
|
149
149
|
expect(result["newrelic"]).toEqual({
|
|
@@ -154,7 +154,7 @@ describe("buildAutoMcpServers", () => {
|
|
|
154
154
|
});
|
|
155
155
|
it("injects New Relic MCP with US (default) region endpoint", () => {
|
|
156
156
|
const result = buildAutoMcpServers(cfg({
|
|
157
|
-
|
|
157
|
+
observabilityProviders: ["newrelic"],
|
|
158
158
|
credentials: { NEW_RELIC_API_KEY: "NRAK-abc" },
|
|
159
159
|
}));
|
|
160
160
|
expect(result["newrelic"]).toEqual({
|
|
@@ -166,7 +166,7 @@ describe("buildAutoMcpServers", () => {
|
|
|
166
166
|
// ── Better Stack MCP ────────────────────────────────────────────
|
|
167
167
|
it("injects Better Stack MCP (HTTP) with Bearer token", () => {
|
|
168
168
|
const result = buildAutoMcpServers(cfg({
|
|
169
|
-
|
|
169
|
+
observabilityProviders: ["betterstack"],
|
|
170
170
|
credentials: { BETTERSTACK_API_TOKEN: "bst_abc" },
|
|
171
171
|
}));
|
|
172
172
|
expect(result["betterstack"]).toEqual({
|
|
@@ -175,9 +175,9 @@ describe("buildAutoMcpServers", () => {
|
|
|
175
175
|
headers: { Authorization: "Bearer bst_abc" },
|
|
176
176
|
});
|
|
177
177
|
});
|
|
178
|
-
it("injects
|
|
178
|
+
it("injects multiple observability MCPs when both are configured", () => {
|
|
179
179
|
const result = buildAutoMcpServers(cfg({
|
|
180
|
-
|
|
180
|
+
observabilityProviders: ["sentry", "betterstack"],
|
|
181
181
|
credentials: { BETTERSTACK_API_TOKEN: "bst_xyz", SENTRY_AUTH_TOKEN: "sntrx_test" },
|
|
182
182
|
}));
|
|
183
183
|
expect(result["betterstack"]).toEqual({
|
|
@@ -329,7 +329,7 @@ describe("buildAutoMcpServers", () => {
|
|
|
329
329
|
const result = buildAutoMcpServers(cfg({
|
|
330
330
|
sourceControlProvider: "github",
|
|
331
331
|
issueTrackerProvider: "linear",
|
|
332
|
-
|
|
332
|
+
observabilityProviders: ["datadog"],
|
|
333
333
|
credentials: {
|
|
334
334
|
GITHUB_TOKEN: "ghp_abc",
|
|
335
335
|
LINEAR_API_KEY: "lin_key_abc",
|
package/dist/types.d.ts
CHANGED
|
@@ -216,7 +216,8 @@ export interface McpServerConfig {
|
|
|
216
216
|
export interface McpAutoConfig {
|
|
217
217
|
sourceControlProvider?: string;
|
|
218
218
|
issueTrackerProvider?: string;
|
|
219
|
-
|
|
219
|
+
/** One or more observability providers (e.g. ["loki", "sentry"]). */
|
|
220
|
+
observabilityProviders?: string[];
|
|
220
221
|
credentials: Record<string, string>;
|
|
221
222
|
workspaceTools?: string[];
|
|
222
223
|
userMcpServers?: Record<string, McpServerConfig>;
|