@sweny-ai/core 0.1.68 → 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.
@@ -23,7 +23,7 @@ describe("reportToCloud", () => {
23
23
  claudeOauthToken: "",
24
24
  openaiApiKey: "",
25
25
  geminiApiKey: "",
26
- observabilityProvider: "",
26
+ observabilityProviders: [],
27
27
  observabilityCredentials: {},
28
28
  issueTrackerProvider: "",
29
29
  linearApiKey: "",
package/dist/cli/check.js CHANGED
@@ -29,21 +29,24 @@ export async function checkProviderConnectivity(config) {
29
29
  });
30
30
  }
31
31
  // ── Observability ────────────────────────────────────────────────────────────
32
- if (config.observabilityProvider === "datadog") {
33
- results.push(await checkDatadog(config.observabilityCredentials));
34
- }
35
- else if (config.observabilityProvider === "sentry") {
36
- results.push(await checkSentry(config.observabilityCredentials));
37
- }
38
- else if (config.observabilityProvider === "file") {
39
- results.push({ name: "Observability (file)", status: "skip", detail: "File provider — no network check needed" });
40
- }
41
- else {
42
- results.push({
43
- name: `Observability (${config.observabilityProvider})`,
44
- status: "skip",
45
- detail: "Connectivity check not implemented for this provider",
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") {
@@ -7,8 +7,8 @@ export interface CliConfig {
7
7
  claudeOauthToken: string;
8
8
  openaiApiKey: string;
9
9
  geminiApiKey: string;
10
- observabilityProvider: string;
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;
@@ -86,7 +86,15 @@ export function parseCliInputs(options, fileConfig = {}) {
86
86
  return [fileVal];
87
87
  return [];
88
88
  };
89
- const obsProvider = options.observabilityProvider || f("observability-provider") || "none";
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
- observabilityProvider: obsProvider,
98
- observabilityCredentials: parseObservabilityCredentials(obsProvider, options, fileConfig),
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
- switch (config.observabilityProvider) {
223
- case "none":
224
- break; // No observability provider configured — nothing to validate
225
- case "datadog":
226
- if (!config.observabilityCredentials.apiKey)
227
- errors.push("Missing: DD_API_KEY — find API keys at https://app.datadoghq.com/organization-settings/api-keys");
228
- if (!config.observabilityCredentials.appKey)
229
- errors.push("Missing: DD_APP_KEY — find application keys at https://app.datadoghq.com/organization-settings/application-keys");
230
- break;
231
- case "sentry":
232
- if (!config.observabilityCredentials.authToken)
233
- errors.push("Missing: SENTRY_AUTH_TOKEN — create a token at https://sentry.io/settings/auth-tokens/");
234
- if (!config.observabilityCredentials.organization)
235
- errors.push("Missing: --sentry-org is required for sentry provider");
236
- if (!config.observabilityCredentials.project)
237
- errors.push("Missing: --sentry-project is required for sentry provider");
238
- break;
239
- case "cloudwatch":
240
- if (!config.observabilityCredentials.logGroupPrefix)
241
- errors.push("Missing: --cloudwatch-log-group-prefix is required for cloudwatch provider");
242
- break;
243
- case "splunk":
244
- if (!config.observabilityCredentials.baseUrl)
245
- errors.push("Missing: SPLUNK_URL is required for splunk provider");
246
- if (!config.observabilityCredentials.token)
247
- errors.push("Missing: SPLUNK_TOKEN is required for splunk provider");
248
- break;
249
- case "elastic":
250
- if (!config.observabilityCredentials.baseUrl)
251
- errors.push("Missing: ELASTIC_URL is required for elastic provider");
252
- if (!config.observabilityCredentials.apiKey)
253
- errors.push("Missing: ELASTIC_API_KEY is required for elastic provider");
254
- break;
255
- case "newrelic":
256
- if (!config.observabilityCredentials.apiKey)
257
- errors.push("Missing: NR_API_KEY is required for newrelic provider");
258
- if (!config.observabilityCredentials.accountId)
259
- errors.push("Missing: NR_ACCOUNT_ID is required for newrelic provider");
260
- break;
261
- case "loki":
262
- if (!config.observabilityCredentials.baseUrl)
263
- errors.push("Missing: LOKI_URL is required for loki provider");
264
- break;
265
- case "file":
266
- if (!config.observabilityCredentials.path)
267
- errors.push("Missing: --log-file <path> is required for file provider");
268
- break;
269
- case "prometheus":
270
- if (!config.observabilityCredentials.url)
271
- errors.push("Missing: PROMETHEUS_URL or --prometheus-url is required for prometheus provider");
272
- break;
273
- case "pagerduty":
274
- if (!config.observabilityCredentials.apiKey)
275
- errors.push("Missing: PAGERDUTY_API_KEY is required for pagerduty provider");
276
- break;
277
- case "heroku":
278
- if (!config.observabilityCredentials.apiKey)
279
- errors.push("Missing: HEROKU_API_KEY is required for heroku provider");
280
- if (!config.observabilityCredentials.appName)
281
- errors.push("Missing: HEROKU_APP_NAME or --heroku-app-name is required for heroku provider");
282
- break;
283
- case "opsgenie":
284
- if (!config.observabilityCredentials.apiKey)
285
- errors.push("Missing: OPSGENIE_API_KEY is required for opsgenie provider");
286
- break;
287
- case "honeycomb":
288
- if (!config.observabilityCredentials.apiKey)
289
- errors.push("Missing: HONEYCOMB_API_KEY is required for honeycomb provider");
290
- if (!config.observabilityCredentials.dataset)
291
- errors.push("Missing: HONEYCOMB_DATASET is required for honeycomb provider");
292
- break;
293
- case "axiom":
294
- if (!config.observabilityCredentials.apiToken)
295
- errors.push("Missing: AXIOM_TOKEN is required for axiom provider");
296
- if (!config.observabilityCredentials.dataset)
297
- errors.push("Missing: AXIOM_DATASET or --axiom-dataset is required for axiom provider");
298
- break;
299
- case "betterstack":
300
- if (!config.observabilityCredentials.apiToken)
301
- errors.push("Missing: BETTERSTACK_API_TOKEN, BETTERSTACK_TELEMETRY_TOKEN, or BETTERSTACK_UPTIME_TOKEN is required for betterstack provider");
302
- if (!config.observabilityCredentials.sourceId && !config.observabilityCredentials.tableName)
303
- errors.push("Missing: either BETTERSTACK_SOURCE_ID (--betterstack-source-id) or BETTERSTACK_TABLE_NAME (--betterstack-table-name) is required for betterstack provider");
304
- break;
305
- case "vercel":
306
- if (!config.observabilityCredentials.token)
307
- errors.push("Missing: VERCEL_TOKEN — create a token at https://vercel.com/account/tokens");
308
- if (!config.observabilityCredentials.projectId)
309
- errors.push("Missing: VERCEL_PROJECT_ID — find it in your Vercel project settings");
310
- break;
311
- case "supabase":
312
- if (!config.observabilityCredentials.managementApiKey)
313
- errors.push("Missing: SUPABASE_MANAGEMENT_KEY — create a token at https://supabase.com/dashboard/account/tokens");
314
- if (!config.observabilityCredentials.projectRef)
315
- errors.push("Missing: SUPABASE_PROJECT_REF — find it in your Supabase project settings > General");
316
- break;
317
- case "netlify":
318
- if (!config.observabilityCredentials.token)
319
- errors.push("Missing: NETLIFY_TOKEN — create a token at https://app.netlify.com/user/applications#personal-access-tokens");
320
- if (!config.observabilityCredentials.siteId)
321
- errors.push("Missing: NETLIFY_SITE_ID — find it in Site Settings > General > Site details");
322
- break;
323
- case "fly":
324
- if (!config.observabilityCredentials.token)
325
- errors.push("Missing: FLY_TOKEN — create a token at https://fly.io/user/personal_access_tokens");
326
- if (!config.observabilityCredentials.appName)
327
- errors.push("Missing: FLY_APP_NAME — the name of your Fly.io application");
328
- break;
329
- case "render":
330
- if (!config.observabilityCredentials.apiKey)
331
- errors.push("Missing: RENDER_API_KEY — create an API key at https://dashboard.render.com/u/settings");
332
- if (!config.observabilityCredentials.serviceId)
333
- errors.push("Missing: RENDER_SERVICE_ID — find it in your service's Settings page (srv-...)");
334
- break;
335
- default:
336
- errors.push(`Unknown --observability-provider "${config.observabilityProvider}". Valid values: none, datadog, sentry, cloudwatch, splunk, elastic, newrelic, loki, prometheus, pagerduty, honeycomb, heroku, opsgenie, axiom, betterstack, vercel, supabase, netlify, fly, render, file`);
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) {
@@ -12,8 +12,8 @@ function baseConfig(overrides = {}) {
12
12
  claudeOauthToken: "",
13
13
  openaiApiKey: "",
14
14
  geminiApiKey: "",
15
- observabilityProvider: "file",
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({ observabilityProvider: "none", observabilityCredentials: {} }));
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({ observabilityProvider: "datadog", observabilityCredentials: {} }));
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 'none' when no provider is configured", () => {
127
+ it("defaults to empty array when no provider is configured", () => {
128
128
  const config = parseCliInputs({}, {});
129
- expect(config.observabilityProvider).toBe("none");
129
+ expect(config.observabilityProviders).toEqual([]);
130
130
  });
131
131
  it("respects explicit datadog provider", () => {
132
132
  const config = parseCliInputs({ observabilityProvider: "datadog" }, {});
133
- expect(config.observabilityProvider).toBe("datadog");
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/main.js CHANGED
@@ -111,7 +111,7 @@ function buildMcpAutoConfig(config) {
111
111
  return {
112
112
  sourceControlProvider: config.sourceControlProvider,
113
113
  issueTrackerProvider: config.issueTrackerProvider,
114
- observabilityProvider: config.observabilityProvider,
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
- if (config.observabilityCredentials.sourceId) {
126
- extras["BetterStack source ID"] = config.observabilityCredentials.sourceId;
125
+ const bsCreds = config.observabilityCredentials["betterstack"];
126
+ if (bsCreds?.sourceId) {
127
+ extras["BetterStack source ID"] = bsCreds.sourceId;
127
128
  }
128
- if (config.observabilityCredentials.tableName) {
129
- extras["BetterStack table name"] = config.observabilityCredentials.tableName;
129
+ if (bsCreds?.tableName) {
130
+ extras["BetterStack table name"] = bsCreds.tableName;
130
131
  }
131
132
  return buildProviderContext({
132
- observabilityProvider: config.observabilityProvider,
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
- observabilityProvider: config.observabilityProvider,
324
- ...(config.observabilityCredentials.sourceId && {
325
- betterstackSourceId: config.observabilityCredentials.sourceId,
325
+ observabilityProviders: config.observabilityProviders,
326
+ ...(bsCreds?.sourceId && {
327
+ betterstackSourceId: bsCreds.sourceId,
326
328
  }),
327
- ...(config.observabilityCredentials.tableName && {
328
- betterstackTableName: config.observabilityCredentials.tableName,
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
- observabilityProvider: config.observabilityProvider,
669
- ...(config.observabilityCredentials.sourceId && {
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.tableName && {
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
  };
@@ -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.observabilityProvider)}`,
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?.observabilityProvider)
464
- rows.push(["Observability", config.observabilityProvider]);
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)
@@ -12,7 +12,7 @@ function minimalConfig(overrides = {}) {
12
12
  timeRange: "6h",
13
13
  serviceFilter: "permit-service",
14
14
  repository: "acme/app",
15
- observabilityProvider: "betterstack",
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
- observabilityProvider?: string;
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 (config.observabilityProvider === "datadog" && ddApiKey && ddAppKey) {
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 (config.observabilityProvider === "sentry" && sentryAuthToken) {
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 (config.observabilityProvider === "newrelic" && nrApiKey) {
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
- if (opts.observabilityProvider) {
366
- const mcpNote = opts.mcpServers.includes(opts.observabilityProvider)
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**: ${opts.observabilityProvider}${mcpNote}`);
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
- observabilityProvider: "datadog",
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({ observabilityProvider: "datadog", credentials: { DD_API_KEY: "dd_api" } }));
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({ observabilityProvider: "sentry", credentials: { SENTRY_AUTH_TOKEN: "sntryu_abc" } }));
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
- observabilityProvider: "sentry",
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
- observabilityProvider: "sentry",
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
- observabilityProvider: "sentry",
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
- observabilityProvider: "newrelic",
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
- observabilityProvider: "newrelic",
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
- observabilityProvider: "betterstack",
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 Better Stack MCP even when primary provider is not betterstack", () => {
178
+ it("injects multiple observability MCPs when both are configured", () => {
179
179
  const result = buildAutoMcpServers(cfg({
180
- observabilityProvider: "sentry",
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
- observabilityProvider: "datadog",
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
- observabilityProvider?: string;
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>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sweny-ai/core",
3
- "version": "0.1.68",
3
+ "version": "0.1.69",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "sweny": "./dist/cli/main.js"