@sweny-ai/core 0.1.7 → 0.1.9

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.
@@ -274,7 +274,7 @@ export function validateInputs(config) {
274
274
  break;
275
275
  case "betterstack":
276
276
  if (!config.observabilityCredentials.apiToken)
277
- errors.push("Missing: BETTERSTACK_API_TOKEN is required for betterstack provider");
277
+ errors.push("Missing: BETTERSTACK_API_TOKEN, BETTERSTACK_TELEMETRY_TOKEN, or BETTERSTACK_UPTIME_TOKEN is required for betterstack provider");
278
278
  if (!config.observabilityCredentials.sourceId && !config.observabilityCredentials.tableName)
279
279
  errors.push("Missing: either BETTERSTACK_SOURCE_ID (--betterstack-source-id) or BETTERSTACK_TABLE_NAME (--betterstack-table-name) is required for betterstack provider");
280
280
  break;
@@ -501,7 +501,7 @@ function parseObservabilityCredentials(provider, options, fileConfig = {}) {
501
501
  };
502
502
  case "betterstack":
503
503
  return {
504
- apiToken: env.BETTERSTACK_API_TOKEN || "",
504
+ apiToken: env.BETTERSTACK_API_TOKEN || env.BETTERSTACK_TELEMETRY_TOKEN || env.BETTERSTACK_UPTIME_TOKEN || "",
505
505
  sourceId: options.betterstackSourceId || env.BETTERSTACK_SOURCE_ID || f("betterstack-source-id") || "",
506
506
  tableName: options.betterstackTableName || env.BETTERSTACK_TABLE_NAME || f("betterstack-table-name") || "",
507
507
  };
package/dist/cli/main.js CHANGED
@@ -108,6 +108,8 @@ function buildCredentialMap() {
108
108
  "NR_API_KEY",
109
109
  "NR_REGION",
110
110
  "BETTERSTACK_API_TOKEN",
111
+ "BETTERSTACK_TELEMETRY_TOKEN",
112
+ "BETTERSTACK_UPTIME_TOKEN",
111
113
  "BETTERSTACK_SOURCE_ID",
112
114
  "BETTERSTACK_TABLE_NAME",
113
115
  "SLACK_BOT_TOKEN",
package/dist/mcp.js CHANGED
@@ -106,9 +106,12 @@ export function buildAutoMcpServers(config) {
106
106
  };
107
107
  }
108
108
  // Better Stack MCP — HTTP remote MCP; Bearer token auth.
109
- // Injected whenever the token is present (not just when it's the primary provider)
109
+ // BetterStack uses separate tokens for Uptime vs Telemetry APIs.
110
+ // The MCP server accepts the telemetry token for log/metric queries.
111
+ // Accept: BETTERSTACK_API_TOKEN (legacy), BETTERSTACK_TELEMETRY_TOKEN, or BETTERSTACK_UPTIME_TOKEN.
112
+ // Injected whenever any token is present (not just when it's the primary provider)
110
113
  // because BetterStack logs complement any primary observability provider.
111
- const bsApiToken = creds.BETTERSTACK_API_TOKEN;
114
+ const bsApiToken = creds.BETTERSTACK_API_TOKEN || creds.BETTERSTACK_TELEMETRY_TOKEN || creds.BETTERSTACK_UPTIME_TOKEN;
112
115
  if (bsApiToken) {
113
116
  auto["betterstack"] = {
114
117
  type: "http",
@@ -0,0 +1,8 @@
1
+ /**
2
+ * BetterStack Skill
3
+ *
4
+ * Telemetry REST API for source management + ClickHouse HTTP for log queries.
5
+ * CI-native alternative to the BetterStack MCP server.
6
+ */
7
+ import type { Skill } from "../types.js";
8
+ export declare const betterstack: Skill;
@@ -0,0 +1,151 @@
1
+ /**
2
+ * BetterStack Skill
3
+ *
4
+ * Telemetry REST API for source management + ClickHouse HTTP for log queries.
5
+ * CI-native alternative to the BetterStack MCP server.
6
+ */
7
+ // ─── REST API (source management) ──────────────────────────────
8
+ async function bsApi(path, ctx) {
9
+ const res = await fetch(`https://telemetry.betterstack.com/api/v1${path}`, {
10
+ headers: { Authorization: `Bearer ${ctx.config.BETTERSTACK_API_TOKEN}` },
11
+ signal: AbortSignal.timeout(30_000),
12
+ });
13
+ if (!res.ok)
14
+ throw new Error(`[BetterStack] API request failed (HTTP ${res.status}): ${await res.text()}`);
15
+ return res.json();
16
+ }
17
+ // ─── ClickHouse HTTP (log queries) ─────────────────────────────
18
+ async function bsQuery(sql, ctx) {
19
+ const endpoint = ctx.config.BETTERSTACK_QUERY_ENDPOINT.replace(/\/+$/, "");
20
+ const res = await fetch(`${endpoint}?output_format_pretty_row_numbers=0`, {
21
+ method: "POST",
22
+ headers: {
23
+ "Content-Type": "text/plain",
24
+ Authorization: `Basic ${btoa(`${ctx.config.BETTERSTACK_QUERY_USERNAME}:${ctx.config.BETTERSTACK_QUERY_PASSWORD}`)}`,
25
+ },
26
+ body: `${sql} FORMAT JSONEachRow`,
27
+ signal: AbortSignal.timeout(30_000),
28
+ });
29
+ if (!res.ok)
30
+ throw new Error(`[BetterStack] ClickHouse query failed (HTTP ${res.status}): ${await res.text()}`);
31
+ // JSONEachRow returns one JSON object per line (NDJSON)
32
+ const text = await res.text();
33
+ if (!text.trim())
34
+ return [];
35
+ return text
36
+ .trim()
37
+ .split("\n")
38
+ .map((line) => JSON.parse(line));
39
+ }
40
+ // ─── Skill definition ──────────────────────────────────────────
41
+ export const betterstack = {
42
+ id: "betterstack",
43
+ name: "BetterStack",
44
+ description: "Query logs and manage telemetry sources in BetterStack",
45
+ category: "observability",
46
+ config: {
47
+ BETTERSTACK_API_TOKEN: {
48
+ description: "BetterStack Telemetry API token (team-scoped)",
49
+ required: true,
50
+ env: "BETTERSTACK_API_TOKEN",
51
+ },
52
+ BETTERSTACK_QUERY_ENDPOINT: {
53
+ description: "ClickHouse HTTP endpoint (e.g. https://eu-fsn-3-connect.betterstackdata.com)",
54
+ required: true,
55
+ env: "BETTERSTACK_QUERY_ENDPOINT",
56
+ },
57
+ BETTERSTACK_QUERY_USERNAME: {
58
+ description: "ClickHouse connection username",
59
+ required: true,
60
+ env: "BETTERSTACK_QUERY_USERNAME",
61
+ },
62
+ BETTERSTACK_QUERY_PASSWORD: {
63
+ description: "ClickHouse connection password",
64
+ required: true,
65
+ env: "BETTERSTACK_QUERY_PASSWORD",
66
+ },
67
+ },
68
+ tools: [
69
+ {
70
+ name: "betterstack_list_sources",
71
+ description: "List available telemetry sources (id, name, table_name, platform)",
72
+ input_schema: {
73
+ type: "object",
74
+ properties: {
75
+ name: { type: "string", description: "Filter by name (partial match)" },
76
+ },
77
+ },
78
+ handler: async (input, ctx) => {
79
+ const params = new URLSearchParams({ per_page: "50" });
80
+ if (input.name)
81
+ params.set("name", input.name);
82
+ const data = await bsApi(`/sources?${params}`, ctx);
83
+ return data.data.map((s) => ({
84
+ id: s.id,
85
+ name: s.attributes.name,
86
+ table_name: s.attributes.table_name,
87
+ platform: s.attributes.platform,
88
+ }));
89
+ },
90
+ },
91
+ {
92
+ name: "betterstack_get_source",
93
+ description: "Get full details for a telemetry source (table name, retention, config)",
94
+ input_schema: {
95
+ type: "object",
96
+ properties: {
97
+ id: { type: "number", description: "Source ID" },
98
+ },
99
+ required: ["id"],
100
+ },
101
+ handler: async (input, ctx) => {
102
+ const data = await bsApi(`/sources/${input.id}`, ctx);
103
+ return { id: data.data.id, ...data.data.attributes };
104
+ },
105
+ },
106
+ {
107
+ name: "betterstack_get_source_fields",
108
+ description: "Get queryable fields for a source table (column names and types)",
109
+ input_schema: {
110
+ type: "object",
111
+ properties: {
112
+ table: { type: "string", description: "Table name (e.g. t273774_offload_ecs_production)" },
113
+ },
114
+ required: ["table"],
115
+ },
116
+ handler: async (input, ctx) => {
117
+ return bsQuery(`DESCRIBE TABLE remote(${input.table}_logs)`, ctx);
118
+ },
119
+ },
120
+ {
121
+ name: "betterstack_query",
122
+ description: `Execute a read-only ClickHouse SQL query against a telemetry source.
123
+ Tables: remote(TABLE_logs) for recent logs, s3Cluster(primary, TABLE_s3) for historical (add WHERE _row_type = 1).
124
+ Key fields: dt (timestamp), raw (JSON blob with all log fields).
125
+ Extract nested fields: JSONExtract(raw, 'field_name', 'Nullable(String)').
126
+ Use betterstack_get_source_fields to discover available columns.`,
127
+ input_schema: {
128
+ type: "object",
129
+ properties: {
130
+ query: { type: "string", description: "ClickHouse SQL query (SELECT only)" },
131
+ source_id: { type: "number", description: "Source ID (for context)" },
132
+ table: { type: "string", description: "Table name (e.g. t273774_offload_ecs_production)" },
133
+ },
134
+ required: ["query", "source_id", "table"],
135
+ },
136
+ handler: async (input, ctx) => {
137
+ const trimmed = input.query.trim();
138
+ const upper = trimmed.toUpperCase();
139
+ if (!upper.startsWith("SELECT") && !upper.startsWith("DESCRIBE")) {
140
+ throw new Error("[BetterStack] Only SELECT and DESCRIBE queries are allowed");
141
+ }
142
+ // Append LIMIT if none present to prevent unbounded result sets
143
+ let sql = trimmed;
144
+ if (!upper.includes("LIMIT")) {
145
+ sql = `${sql} LIMIT 500`;
146
+ }
147
+ return bsQuery(sql, ctx);
148
+ },
149
+ },
150
+ ],
151
+ };
@@ -9,9 +9,10 @@ import { linear } from "./linear.js";
9
9
  import { slack } from "./slack.js";
10
10
  import { sentry } from "./sentry.js";
11
11
  import { datadog } from "./datadog.js";
12
+ import { betterstack } from "./betterstack.js";
12
13
  import { notification } from "./notification.js";
13
14
  export declare const builtinSkills: Skill[];
14
- export { github, linear, slack, sentry, datadog, notification };
15
+ export { github, linear, slack, sentry, datadog, betterstack, notification };
15
16
  /**
16
17
  * Build a skill map from an array of skills.
17
18
  * Pass to `execute()` as the `skills` option.
@@ -8,10 +8,11 @@ import { linear } from "./linear.js";
8
8
  import { slack } from "./slack.js";
9
9
  import { sentry } from "./sentry.js";
10
10
  import { datadog } from "./datadog.js";
11
+ import { betterstack } from "./betterstack.js";
11
12
  import { notification } from "./notification.js";
12
13
  // ─── Built-in skill catalog ─────────────────────────────────────
13
- export const builtinSkills = [github, linear, slack, sentry, datadog, notification];
14
- export { github, linear, slack, sentry, datadog, notification };
14
+ export const builtinSkills = [github, linear, slack, sentry, datadog, betterstack, notification];
15
+ export { github, linear, slack, sentry, datadog, betterstack, notification };
15
16
  // ─── Registry helpers ───────────────────────────────────────────
16
17
  /**
17
18
  * Build a skill map from an array of skills.
@@ -79,6 +79,41 @@ export const linear = {
79
79
  },
80
80
  handler: async (input, ctx) => linearGql(`mutation($input: CommentCreateInput!) { commentCreate(input: $input) { success comment { id body } } }`, { input: { issueId: input.issueId, body: input.body } }, ctx),
81
81
  },
82
+ {
83
+ name: "linear_get_issue",
84
+ description: "Get a Linear issue by ID or identifier (e.g. 'OFF-1020')",
85
+ input_schema: {
86
+ type: "object",
87
+ properties: {
88
+ id: { type: "string", description: "Linear issue ID (UUID) or identifier (e.g. 'OFF-1020')" },
89
+ },
90
+ required: ["id"],
91
+ },
92
+ handler: async (input, ctx) => linearGql(`query($id: String!) {
93
+ issue(id: $id) {
94
+ id identifier title url description
95
+ state { name type }
96
+ priority priorityLabel
97
+ assignee { name email }
98
+ labels { nodes { name } }
99
+ team { key name }
100
+ createdAt updatedAt
101
+ }
102
+ }`, { id: input.id }, ctx),
103
+ },
104
+ {
105
+ name: "linear_list_teams",
106
+ description: "List Linear teams (needed for teamId when creating issues)",
107
+ input_schema: {
108
+ type: "object",
109
+ properties: {},
110
+ },
111
+ handler: async (_input, ctx) => linearGql(`query {
112
+ teams {
113
+ nodes { id key name description }
114
+ }
115
+ }`, {}, ctx),
116
+ },
82
117
  {
83
118
  name: "linear_update_issue",
84
119
  description: "Update an existing Linear issue",
@@ -43,7 +43,7 @@ If there are no URLs to fetch, just pass through — this step is a no-op.`,
43
43
  3. **Issue tracker**: Search for similar past issues or known problems.
44
44
 
45
45
  Be thorough — the investigation step depends on complete context. Use every tool available to you.`,
46
- skills: ["github", "sentry", "datadog", "linear"],
46
+ skills: ["github", "sentry", "datadog", "betterstack", "linear"],
47
47
  },
48
48
  investigate: {
49
49
  name: "Root Cause Analysis",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sweny-ai/core",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "sweny": "./dist/cli/main.js"