@sweny-ai/core 0.1.7 → 0.1.8

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.
@@ -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.8",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "sweny": "./dist/cli/main.js"