@nimbus-dev/connectors 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +662 -0
- package/NOTICE +40 -0
- package/README.md +111 -0
- package/connectors/airflow/nimbus.extension.json +16 -0
- package/connectors/airflow/src/search-filter.ts +38 -0
- package/connectors/airflow/src/server.ts +91 -0
- package/connectors/apple/nimbus.extension.json +40 -0
- package/connectors/apple/src/apple-mail-core.ts +43 -0
- package/connectors/apple/src/caldav-core.ts +116 -0
- package/connectors/apple/src/calendar-tools.ts +278 -0
- package/connectors/apple/src/server.ts +541 -0
- package/connectors/apple/src/tools.ts +138 -0
- package/connectors/argocd/nimbus.extension.json +16 -0
- package/connectors/argocd/src/search-filter.ts +24 -0
- package/connectors/argocd/src/server.ts +158 -0
- package/connectors/athena/nimbus.extension.json +15 -0
- package/connectors/athena/src/server.ts +6 -0
- package/connectors/athena/src/tools.ts +181 -0
- package/connectors/aws/nimbus.extension.json +21 -0
- package/connectors/aws/src/server.ts +191 -0
- package/connectors/azure/nimbus.extension.json +15 -0
- package/connectors/azure/src/server.ts +139 -0
- package/connectors/bigeye/nimbus.extension.json +16 -0
- package/connectors/bigeye/src/search-filter.ts +22 -0
- package/connectors/bigeye/src/server.ts +161 -0
- package/connectors/bigquery/nimbus.extension.json +15 -0
- package/connectors/bigquery/src/server.ts +6 -0
- package/connectors/bigquery/src/tools.ts +175 -0
- package/connectors/bitbucket/nimbus.extension.json +15 -0
- package/connectors/bitbucket/src/server.ts +252 -0
- package/connectors/bitrise/nimbus.extension.json +15 -0
- package/connectors/bitrise/src/search-filter.ts +11 -0
- package/connectors/bitrise/src/server.ts +83 -0
- package/connectors/canva/nimbus.extension.json +15 -0
- package/connectors/canva/src/search-filter.ts +23 -0
- package/connectors/canva/src/server.ts +77 -0
- package/connectors/circleci/nimbus.extension.json +15 -0
- package/connectors/circleci/src/server.ts +184 -0
- package/connectors/cloud-logging/nimbus.extension.json +15 -0
- package/connectors/cloud-logging/src/server.ts +6 -0
- package/connectors/cloud-logging/src/tools.ts +115 -0
- package/connectors/cloudwatch/nimbus.extension.json +15 -0
- package/connectors/cloudwatch/src/server.ts +6 -0
- package/connectors/cloudwatch/src/tools.ts +144 -0
- package/connectors/codemagic/nimbus.extension.json +15 -0
- package/connectors/codemagic/src/search-filter.ts +11 -0
- package/connectors/codemagic/src/server.ts +74 -0
- package/connectors/confluence/nimbus.extension.json +15 -0
- package/connectors/confluence/src/kb-append.ts +25 -0
- package/connectors/confluence/src/server.ts +309 -0
- package/connectors/dagster/nimbus.extension.json +16 -0
- package/connectors/dagster/src/search-filter.ts +53 -0
- package/connectors/dagster/src/server.ts +218 -0
- package/connectors/databricks/nimbus.extension.json +16 -0
- package/connectors/databricks/src/search-filter.ts +28 -0
- package/connectors/databricks/src/server.ts +81 -0
- package/connectors/datadog/nimbus.extension.json +15 -0
- package/connectors/datadog/src/server.ts +56 -0
- package/connectors/dataprofile/nimbus.extension.json +16 -0
- package/connectors/dataprofile/src/profile.ts +298 -0
- package/connectors/dataprofile/src/server.ts +6 -0
- package/connectors/dataprofile/src/tools.ts +69 -0
- package/connectors/dbt/nimbus.extension.json +16 -0
- package/connectors/dbt/src/search-filter.ts +28 -0
- package/connectors/dbt/src/server.ts +96 -0
- package/connectors/dependencytrack/nimbus.extension.json +16 -0
- package/connectors/dependencytrack/src/search-filter.ts +24 -0
- package/connectors/dependencytrack/src/server.ts +80 -0
- package/connectors/discord/nimbus.extension.json +15 -0
- package/connectors/discord/src/server.ts +93 -0
- package/connectors/elasticsearch/nimbus.extension.json +16 -0
- package/connectors/elasticsearch/src/server.ts +6 -0
- package/connectors/elasticsearch/src/tools.ts +110 -0
- package/connectors/fastmail/nimbus.extension.json +16 -0
- package/connectors/fastmail/src/jmap-core.ts +86 -0
- package/connectors/fastmail/src/server.ts +194 -0
- package/connectors/fastmail/src/tools.ts +143 -0
- package/connectors/figma/nimbus.extension.json +15 -0
- package/connectors/figma/src/search-filter.ts +24 -0
- package/connectors/figma/src/server.ts +117 -0
- package/connectors/firebase/nimbus.extension.json +15 -0
- package/connectors/firebase/src/search-filter.ts +30 -0
- package/connectors/firebase/src/server.ts +94 -0
- package/connectors/firebase/src/token.ts +41 -0
- package/connectors/flagsmith/nimbus.extension.json +15 -0
- package/connectors/flagsmith/src/search-filter.ts +29 -0
- package/connectors/flagsmith/src/server.ts +102 -0
- package/connectors/flux/nimbus.extension.json +16 -0
- package/connectors/flux/src/search-filter.ts +42 -0
- package/connectors/flux/src/server.ts +182 -0
- package/connectors/gcp/nimbus.extension.json +15 -0
- package/connectors/gcp/src/server.ts +140 -0
- package/connectors/github/nimbus.extension.json +15 -0
- package/connectors/github/src/server.ts +347 -0
- package/connectors/github-actions/nimbus.extension.json +15 -0
- package/connectors/github-actions/src/server.ts +213 -0
- package/connectors/gitlab/nimbus.extension.json +15 -0
- package/connectors/gitlab/src/server.ts +392 -0
- package/connectors/gmail/nimbus.extension.json +15 -0
- package/connectors/gmail/src/server.ts +277 -0
- package/connectors/google-drive/nimbus.extension.json +15 -0
- package/connectors/google-drive/src/drive-query.ts +3 -0
- package/connectors/google-drive/src/server.ts +531 -0
- package/connectors/google-meet/nimbus.extension.json +15 -0
- package/connectors/google-meet/src/server.ts +89 -0
- package/connectors/google-photos/nimbus.extension.json +15 -0
- package/connectors/google-photos/src/server.ts +139 -0
- package/connectors/grafana/nimbus.extension.json +15 -0
- package/connectors/grafana/src/server.ts +61 -0
- package/connectors/great-expectations/nimbus.extension.json +16 -0
- package/connectors/great-expectations/src/gx-parse.ts +292 -0
- package/connectors/great-expectations/src/server.ts +6 -0
- package/connectors/great-expectations/src/tools.ts +91 -0
- package/connectors/greenhouse/nimbus.extension.json +15 -0
- package/connectors/greenhouse/src/search-filter.ts +65 -0
- package/connectors/greenhouse/src/server.ts +67 -0
- package/connectors/hubspot/nimbus.extension.json +15 -0
- package/connectors/hubspot/src/search-filter.ts +28 -0
- package/connectors/hubspot/src/server.ts +81 -0
- package/connectors/iac/nimbus.extension.json +14 -0
- package/connectors/iac/src/server.ts +145 -0
- package/connectors/imap/nimbus.extension.json +16 -0
- package/connectors/imap/src/imap-core.ts +110 -0
- package/connectors/imap/src/server.ts +232 -0
- package/connectors/imap/src/tools.ts +40 -0
- package/connectors/intercom/nimbus.extension.json +15 -0
- package/connectors/intercom/src/search-filter.ts +46 -0
- package/connectors/intercom/src/server.ts +65 -0
- package/connectors/jenkins/nimbus.extension.json +15 -0
- package/connectors/jenkins/src/jenkins-api.ts +156 -0
- package/connectors/jenkins/src/server.ts +223 -0
- package/connectors/jira/nimbus.extension.json +15 -0
- package/connectors/jira/src/server.ts +309 -0
- package/connectors/kubernetes/nimbus.extension.json +15 -0
- package/connectors/kubernetes/src/server.ts +163 -0
- package/connectors/launchdarkly/nimbus.extension.json +15 -0
- package/connectors/launchdarkly/src/search-filter.ts +31 -0
- package/connectors/launchdarkly/src/server.ts +79 -0
- package/connectors/lever/nimbus.extension.json +15 -0
- package/connectors/lever/src/search-filter.ts +35 -0
- package/connectors/lever/src/server.ts +64 -0
- package/connectors/linear/nimbus.extension.json +15 -0
- package/connectors/linear/src/server.ts +467 -0
- package/connectors/localdb/nimbus.extension.json +16 -0
- package/connectors/localdb/src/server.ts +6 -0
- package/connectors/localdb/src/sql-scan.ts +158 -0
- package/connectors/localdb/src/tools.ts +67 -0
- package/connectors/looker/nimbus.extension.json +16 -0
- package/connectors/looker/src/search-filter.ts +22 -0
- package/connectors/looker/src/server.ts +254 -0
- package/connectors/mendeley/nimbus.extension.json +15 -0
- package/connectors/mendeley/src/search-filter.ts +68 -0
- package/connectors/mendeley/src/server.ts +59 -0
- package/connectors/mercury/nimbus.extension.json +15 -0
- package/connectors/mercury/src/search-filter.ts +11 -0
- package/connectors/mercury/src/server.ts +61 -0
- package/connectors/metabase/nimbus.extension.json +16 -0
- package/connectors/metabase/src/search-filter.ts +22 -0
- package/connectors/metabase/src/server.ts +83 -0
- package/connectors/miro/nimbus.extension.json +15 -0
- package/connectors/miro/src/search-filter.ts +24 -0
- package/connectors/miro/src/server.ts +76 -0
- package/connectors/mlflow/nimbus.extension.json +16 -0
- package/connectors/mlflow/src/search-filter.ts +38 -0
- package/connectors/mlflow/src/server.ts +167 -0
- package/connectors/monte-carlo/nimbus.extension.json +16 -0
- package/connectors/monte-carlo/src/search-filter.ts +11 -0
- package/connectors/monte-carlo/src/server.ts +241 -0
- package/connectors/netlify/nimbus.extension.json +15 -0
- package/connectors/netlify/src/search-filter.ts +37 -0
- package/connectors/netlify/src/server.ts +64 -0
- package/connectors/newrelic/nimbus.extension.json +15 -0
- package/connectors/newrelic/src/server.ts +48 -0
- package/connectors/notion/nimbus.extension.json +15 -0
- package/connectors/notion/src/kb-append.ts +23 -0
- package/connectors/notion/src/server.ts +343 -0
- package/connectors/obsidian/nimbus.extension.json +19 -0
- package/connectors/obsidian/src/server.ts +433 -0
- package/connectors/onedrive/nimbus.extension.json +15 -0
- package/connectors/onedrive/src/server.ts +256 -0
- package/connectors/outlook/nimbus.extension.json +15 -0
- package/connectors/outlook/src/server.ts +358 -0
- package/connectors/outlook/src/tool-scope-policy.ts +53 -0
- package/connectors/pagerduty/nimbus.extension.json +15 -0
- package/connectors/pagerduty/src/server.ts +140 -0
- package/connectors/pipedrive/nimbus.extension.json +15 -0
- package/connectors/pipedrive/src/search-filter.ts +11 -0
- package/connectors/pipedrive/src/server.ts +62 -0
- package/connectors/powerbi/nimbus.extension.json +16 -0
- package/connectors/powerbi/src/search-filter.ts +22 -0
- package/connectors/powerbi/src/server.ts +249 -0
- package/connectors/prefect/nimbus.extension.json +16 -0
- package/connectors/prefect/src/search-filter.ts +40 -0
- package/connectors/prefect/src/server.ts +94 -0
- package/connectors/protonmail/nimbus.extension.json +16 -0
- package/connectors/protonmail/src/mail-core.ts +96 -0
- package/connectors/protonmail/src/server.ts +231 -0
- package/connectors/protonmail/src/tools.ts +45 -0
- package/connectors/raindrop/nimbus.extension.json +15 -0
- package/connectors/raindrop/src/search-filter.ts +20 -0
- package/connectors/raindrop/src/server.ts +107 -0
- package/connectors/ramp/nimbus.extension.json +15 -0
- package/connectors/ramp/src/search-filter.ts +37 -0
- package/connectors/ramp/src/server.ts +109 -0
- package/connectors/readwise/nimbus.extension.json +15 -0
- package/connectors/readwise/src/search-filter.ts +64 -0
- package/connectors/readwise/src/server.ts +92 -0
- package/connectors/sagemaker/nimbus.extension.json +15 -0
- package/connectors/sagemaker/src/server.ts +6 -0
- package/connectors/sagemaker/src/tools.ts +121 -0
- package/connectors/salesforce/nimbus.extension.json +15 -0
- package/connectors/salesforce/src/search-filter.ts +23 -0
- package/connectors/salesforce/src/server.ts +94 -0
- package/connectors/semgrep/nimbus.extension.json +15 -0
- package/connectors/semgrep/src/search-filter.ts +31 -0
- package/connectors/semgrep/src/server.ts +108 -0
- package/connectors/sentry/nimbus.extension.json +15 -0
- package/connectors/sentry/src/server.ts +66 -0
- package/connectors/slack/nimbus.extension.json +15 -0
- package/connectors/slack/src/server.ts +360 -0
- package/connectors/snowflake/nimbus.extension.json +16 -0
- package/connectors/snowflake/src/search-filter.ts +26 -0
- package/connectors/snowflake/src/server.ts +236 -0
- package/connectors/snyk/nimbus.extension.json +15 -0
- package/connectors/snyk/src/search-filter.ts +28 -0
- package/connectors/snyk/src/server.ts +144 -0
- package/connectors/sonarqube/nimbus.extension.json +15 -0
- package/connectors/sonarqube/src/search-filter.ts +28 -0
- package/connectors/sonarqube/src/server.ts +115 -0
- package/connectors/stackoverflow/nimbus.extension.json +15 -0
- package/connectors/stackoverflow/src/search-filter.ts +52 -0
- package/connectors/stackoverflow/src/server.ts +81 -0
- package/connectors/storybook/nimbus.extension.json +16 -0
- package/connectors/storybook/src/server.ts +6 -0
- package/connectors/storybook/src/storybook-parse.ts +67 -0
- package/connectors/storybook/src/tools.ts +68 -0
- package/connectors/stripe/nimbus.extension.json +15 -0
- package/connectors/stripe/src/search-filter.ts +19 -0
- package/connectors/stripe/src/server.ts +65 -0
- package/connectors/superset/nimbus.extension.json +16 -0
- package/connectors/superset/src/search-filter.ts +22 -0
- package/connectors/superset/src/server.ts +119 -0
- package/connectors/tableau/nimbus.extension.json +16 -0
- package/connectors/tableau/src/search-filter.ts +22 -0
- package/connectors/tableau/src/server.ts +248 -0
- package/connectors/teams/nimbus.extension.json +20 -0
- package/connectors/teams/src/bot-send.ts +72 -0
- package/connectors/teams/src/server.ts +257 -0
- package/connectors/testflight/nimbus.extension.json +15 -0
- package/connectors/testflight/src/jwt.ts +27 -0
- package/connectors/testflight/src/search-filter.ts +30 -0
- package/connectors/testflight/src/server.ts +74 -0
- package/connectors/vercel/nimbus.extension.json +15 -0
- package/connectors/vercel/src/search-filter.ts +34 -0
- package/connectors/vercel/src/server.ts +79 -0
- package/connectors/vertex-ai/nimbus.extension.json +15 -0
- package/connectors/vertex-ai/src/server.ts +6 -0
- package/connectors/vertex-ai/src/tools.ts +145 -0
- package/connectors/wiz/nimbus.extension.json +15 -0
- package/connectors/wiz/src/search-filter.ts +50 -0
- package/connectors/wiz/src/server.ts +192 -0
- package/connectors/workday/nimbus.extension.json +19 -0
- package/connectors/workday/src/search-filter.ts +5 -0
- package/connectors/workday/src/server.ts +78 -0
- package/connectors/workday/src/tools.ts +1 -0
- package/connectors/zendesk/nimbus.extension.json +16 -0
- package/connectors/zendesk/src/search-filter.ts +11 -0
- package/connectors/zendesk/src/server.ts +77 -0
- package/connectors/zoom/nimbus.extension.json +15 -0
- package/connectors/zoom/src/encode-meeting-path.ts +5 -0
- package/connectors/zoom/src/recordings-window.ts +34 -0
- package/connectors/zoom/src/search-filter.ts +42 -0
- package/connectors/zoom/src/server.ts +98 -0
- package/connectors/zotero/nimbus.extension.json +15 -0
- package/connectors/zotero/src/search-filter.ts +81 -0
- package/connectors/zotero/src/server.ts +82 -0
- package/package.json +152 -0
- package/shared/atlassian-json-fetch.ts +42 -0
- package/shared/audit-chain.ts +133 -0
- package/shared/connector-mode.ts +60 -0
- package/shared/consent-kit.ts +323 -0
- package/shared/fetch-bearer-json.ts +18 -0
- package/shared/header-safe.ts +28 -0
- package/shared/imap-bodystructure.ts +113 -0
- package/shared/imap-mail-core.ts +57 -0
- package/shared/imap-tool-kit.ts +293 -0
- package/shared/join-api-path.ts +18 -0
- package/shared/kb-markdown.ts +142 -0
- package/shared/mcp-search-tool.ts +37 -0
- package/shared/mcp-tool-kit.ts +35 -0
- package/shared/nimbus-spawn.ts +128 -0
- package/shared/rest-tool-kit.ts +18 -0
- package/shared/run-cli-json.ts +55 -0
- package/shared/run-read-only-mcp-connector.ts +59 -0
- package/shared/safe-cli-arg.ts +55 -0
- package/shared/search-filter.ts +140 -0
- package/shared/strip-trailing-slashes.ts +7 -0
- package/shared/write-scope.ts +58 -0
- package/standalone/src/bin.ts +19 -0
- package/standalone/src/launcher.ts +186 -0
- package/standalone/tsconfig.json +8 -0
|
@@ -0,0 +1,467 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
createRegisterSimpleTool,
|
|
7
|
+
createZodToolRegistrar,
|
|
8
|
+
mcpJsonResult as jsonResult,
|
|
9
|
+
requireProcessEnv,
|
|
10
|
+
} from "../../../shared/mcp-tool-kit.ts";
|
|
11
|
+
|
|
12
|
+
const LINEAR_GQL = "https://api.linear.app/graphql";
|
|
13
|
+
|
|
14
|
+
type LinearGqlResponse<T> = {
|
|
15
|
+
data?: T;
|
|
16
|
+
errors?: ReadonlyArray<{ message: string }>;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
async function linearGraphql<T>(
|
|
20
|
+
apiKey: string,
|
|
21
|
+
query: string,
|
|
22
|
+
variables?: Record<string, unknown>,
|
|
23
|
+
): Promise<{ ok: true; data: T; text: string } | { ok: false; status: number; text: string }> {
|
|
24
|
+
const res = await fetch(LINEAR_GQL, {
|
|
25
|
+
method: "POST",
|
|
26
|
+
headers: {
|
|
27
|
+
"Content-Type": "application/json",
|
|
28
|
+
Authorization: apiKey,
|
|
29
|
+
},
|
|
30
|
+
body: JSON.stringify({ query, variables }),
|
|
31
|
+
});
|
|
32
|
+
const text = await res.text();
|
|
33
|
+
if (!res.ok) {
|
|
34
|
+
return { ok: false, status: res.status, text };
|
|
35
|
+
}
|
|
36
|
+
let parsed: unknown;
|
|
37
|
+
try {
|
|
38
|
+
parsed = JSON.parse(text) as unknown;
|
|
39
|
+
} catch {
|
|
40
|
+
return { ok: false, status: res.status, text: "invalid JSON" };
|
|
41
|
+
}
|
|
42
|
+
const body = parsed as LinearGqlResponse<T>;
|
|
43
|
+
if (body.errors !== undefined && body.errors.length > 0) {
|
|
44
|
+
const msg = body.errors.map((e) => e.message).join("; ");
|
|
45
|
+
throw new Error(`Linear GraphQL: ${msg.slice(0, 400)}`);
|
|
46
|
+
}
|
|
47
|
+
if (body.data === undefined) {
|
|
48
|
+
throw new Error("Linear GraphQL: missing data");
|
|
49
|
+
}
|
|
50
|
+
return { ok: true, data: body.data, text };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function linearGqlData<T>(
|
|
54
|
+
res: { ok: true; data: T; text: string } | { ok: false; status: number; text: string },
|
|
55
|
+
): T {
|
|
56
|
+
if (!res.ok) {
|
|
57
|
+
throw new Error(`Linear ${String(res.status)}: ${res.text.slice(0, 300)}`);
|
|
58
|
+
}
|
|
59
|
+
return res.data;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const server = new McpServer({ name: "nimbus-linear", version: "0.1.0" });
|
|
63
|
+
|
|
64
|
+
const registerSimpleTool = createRegisterSimpleTool(server);
|
|
65
|
+
|
|
66
|
+
import { createWriteToolRegistrar } from "../../../shared/consent-kit.ts";
|
|
67
|
+
|
|
68
|
+
const reg = createZodToolRegistrar(registerSimpleTool);
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Every MUTATING linear tool goes through here. Outside the gateway this adds the
|
|
72
|
+
* consent gate, the write-scope allow-list, the mutation budget and the audit record; inside
|
|
73
|
+
* the gateway it is a pass-through, because executor.ts (I2) is the gate there.
|
|
74
|
+
*/
|
|
75
|
+
const registerWriteTool = createWriteToolRegistrar(server, {
|
|
76
|
+
connector: "linear",
|
|
77
|
+
scopeEnv: "NIMBUS_MCP_LINEAR_WRITE_SCOPE",
|
|
78
|
+
scopeKinds: ["team", "issue"],
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const linearIssueListSchema = z.object({
|
|
82
|
+
first: z.number().int().min(1).max(100).optional(),
|
|
83
|
+
teamId: z.string().min(1).optional(),
|
|
84
|
+
stateName: z.string().min(1).optional(),
|
|
85
|
+
assigneeId: z.string().min(1).optional(),
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
reg(
|
|
89
|
+
"linear_issue_list",
|
|
90
|
+
"List Linear issues with optional filters (team id, state name, assignee id).",
|
|
91
|
+
linearIssueListSchema,
|
|
92
|
+
async (parsed) => {
|
|
93
|
+
const apiKey = requireProcessEnv("LINEAR_API_KEY");
|
|
94
|
+
const filter: Record<string, unknown> = {};
|
|
95
|
+
if (parsed.teamId !== undefined) {
|
|
96
|
+
filter["team"] = { id: { eq: parsed.teamId } };
|
|
97
|
+
}
|
|
98
|
+
if (parsed.stateName !== undefined) {
|
|
99
|
+
filter["state"] = { name: { eq: parsed.stateName } };
|
|
100
|
+
}
|
|
101
|
+
if (parsed.assigneeId !== undefined) {
|
|
102
|
+
filter["assignee"] = { id: { eq: parsed.assigneeId } };
|
|
103
|
+
}
|
|
104
|
+
const hasFilter = Object.keys(filter).length > 0;
|
|
105
|
+
const q = hasFilter
|
|
106
|
+
? `
|
|
107
|
+
query IssueList($first: Int!, $filter: IssueFilter!) {
|
|
108
|
+
issues(first: $first, filter: $filter, orderBy: updatedAt) {
|
|
109
|
+
nodes {
|
|
110
|
+
id
|
|
111
|
+
identifier
|
|
112
|
+
title
|
|
113
|
+
updatedAt
|
|
114
|
+
url
|
|
115
|
+
state { id name type }
|
|
116
|
+
team { id name key }
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
`
|
|
121
|
+
: `
|
|
122
|
+
query IssueList($first: Int!) {
|
|
123
|
+
issues(first: $first, orderBy: updatedAt) {
|
|
124
|
+
nodes {
|
|
125
|
+
id
|
|
126
|
+
identifier
|
|
127
|
+
title
|
|
128
|
+
updatedAt
|
|
129
|
+
url
|
|
130
|
+
state { id name type }
|
|
131
|
+
team { id name key }
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
`;
|
|
136
|
+
type Out = {
|
|
137
|
+
issues: {
|
|
138
|
+
nodes: ReadonlyArray<Record<string, unknown>>;
|
|
139
|
+
};
|
|
140
|
+
};
|
|
141
|
+
const vars: Record<string, unknown> = { first: parsed.first ?? 50 };
|
|
142
|
+
if (hasFilter) {
|
|
143
|
+
vars["filter"] = filter;
|
|
144
|
+
}
|
|
145
|
+
const res = await linearGraphql<Out>(apiKey, q, vars);
|
|
146
|
+
return jsonResult(linearGqlData(res));
|
|
147
|
+
},
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
const linearIssueIdSchema = z.object({ issueId: z.string().min(1) });
|
|
151
|
+
|
|
152
|
+
reg(
|
|
153
|
+
"linear_issue_get",
|
|
154
|
+
"Get a single Linear issue by UUID id.",
|
|
155
|
+
linearIssueIdSchema,
|
|
156
|
+
async (parsed) => {
|
|
157
|
+
const apiKey = requireProcessEnv("LINEAR_API_KEY");
|
|
158
|
+
const q = `
|
|
159
|
+
query IssueGet($id: String!) {
|
|
160
|
+
issue(id: $id) {
|
|
161
|
+
id
|
|
162
|
+
identifier
|
|
163
|
+
title
|
|
164
|
+
description
|
|
165
|
+
updatedAt
|
|
166
|
+
createdAt
|
|
167
|
+
url
|
|
168
|
+
priority
|
|
169
|
+
state { id name type }
|
|
170
|
+
team { id name key }
|
|
171
|
+
assignee { id name displayName }
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
`;
|
|
175
|
+
type Out = { issue: Record<string, unknown> | null };
|
|
176
|
+
const res = await linearGraphql<Out>(apiKey, q, { id: parsed.issueId });
|
|
177
|
+
return jsonResult(linearGqlData(res));
|
|
178
|
+
},
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
const linearIssueCreateSchema = z.object({
|
|
182
|
+
teamId: z.string().min(1),
|
|
183
|
+
title: z.string().min(1),
|
|
184
|
+
description: z.string().optional(),
|
|
185
|
+
priority: z.number().int().min(0).max(4).optional(),
|
|
186
|
+
stateId: z.string().min(1).optional(),
|
|
187
|
+
assigneeId: z.string().min(1).optional(),
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
registerWriteTool(
|
|
191
|
+
"linear_issue_create",
|
|
192
|
+
{
|
|
193
|
+
mutates: "linear.issue.create",
|
|
194
|
+
recoverable: true,
|
|
195
|
+
scopeTargetOf: (p) => ({ kind: "team", value: p.teamId }),
|
|
196
|
+
},
|
|
197
|
+
"Create a Linear issue (requires teamId and title).",
|
|
198
|
+
linearIssueCreateSchema,
|
|
199
|
+
async (parsed) => {
|
|
200
|
+
const apiKey = requireProcessEnv("LINEAR_API_KEY");
|
|
201
|
+
const input: Record<string, unknown> = {
|
|
202
|
+
teamId: parsed.teamId,
|
|
203
|
+
title: parsed.title,
|
|
204
|
+
};
|
|
205
|
+
if (parsed.description !== undefined) {
|
|
206
|
+
input["description"] = parsed.description;
|
|
207
|
+
}
|
|
208
|
+
if (parsed.priority !== undefined) {
|
|
209
|
+
input["priority"] = parsed.priority;
|
|
210
|
+
}
|
|
211
|
+
if (parsed.stateId !== undefined) {
|
|
212
|
+
input["stateId"] = parsed.stateId;
|
|
213
|
+
}
|
|
214
|
+
if (parsed.assigneeId !== undefined) {
|
|
215
|
+
input["assigneeId"] = parsed.assigneeId;
|
|
216
|
+
}
|
|
217
|
+
const q = `
|
|
218
|
+
mutation IssueCreate($input: IssueCreateInput!) {
|
|
219
|
+
issueCreate(input: $input) {
|
|
220
|
+
success
|
|
221
|
+
issue { id identifier url title }
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
`;
|
|
225
|
+
type Out = { issueCreate: { success: boolean; issue: Record<string, unknown> | null } };
|
|
226
|
+
const res = await linearGraphql<Out>(apiKey, q, { input });
|
|
227
|
+
return jsonResult(linearGqlData(res));
|
|
228
|
+
},
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
const linearIssueUpdateSchema = z.object({
|
|
232
|
+
issueId: z.string().min(1),
|
|
233
|
+
title: z.string().min(1).optional(),
|
|
234
|
+
description: z.string().optional(),
|
|
235
|
+
stateId: z.string().min(1).optional(),
|
|
236
|
+
priority: z.number().int().min(0).max(4).optional(),
|
|
237
|
+
assigneeId: z.string().min(1).optional(),
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
registerWriteTool(
|
|
241
|
+
"linear_issue_update",
|
|
242
|
+
{
|
|
243
|
+
mutates: "linear.issue.update",
|
|
244
|
+
recoverable: true,
|
|
245
|
+
scopeTargetOf: (p) => ({ kind: "issue", value: p.issueId }),
|
|
246
|
+
},
|
|
247
|
+
"Update a Linear issue by id.",
|
|
248
|
+
linearIssueUpdateSchema,
|
|
249
|
+
async (parsed) => {
|
|
250
|
+
const apiKey = requireProcessEnv("LINEAR_API_KEY");
|
|
251
|
+
const input: Record<string, unknown> = {};
|
|
252
|
+
if (parsed.title !== undefined) {
|
|
253
|
+
input["title"] = parsed.title;
|
|
254
|
+
}
|
|
255
|
+
if (parsed.description !== undefined) {
|
|
256
|
+
input["description"] = parsed.description;
|
|
257
|
+
}
|
|
258
|
+
if (parsed.stateId !== undefined) {
|
|
259
|
+
input["stateId"] = parsed.stateId;
|
|
260
|
+
}
|
|
261
|
+
if (parsed.priority !== undefined) {
|
|
262
|
+
input["priority"] = parsed.priority;
|
|
263
|
+
}
|
|
264
|
+
if (parsed.assigneeId !== undefined) {
|
|
265
|
+
input["assigneeId"] = parsed.assigneeId;
|
|
266
|
+
}
|
|
267
|
+
const q = `
|
|
268
|
+
mutation IssueUpdate($id: String!, $input: IssueUpdateInput!) {
|
|
269
|
+
issueUpdate(id: $id, input: $input) {
|
|
270
|
+
success
|
|
271
|
+
issue { id identifier url title updatedAt }
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
`;
|
|
275
|
+
type Out = {
|
|
276
|
+
issueUpdate: { success: boolean; issue: Record<string, unknown> | null };
|
|
277
|
+
};
|
|
278
|
+
const res = await linearGraphql<Out>(apiKey, q, {
|
|
279
|
+
id: parsed.issueId,
|
|
280
|
+
input,
|
|
281
|
+
});
|
|
282
|
+
return jsonResult(linearGqlData(res));
|
|
283
|
+
},
|
|
284
|
+
);
|
|
285
|
+
|
|
286
|
+
const linearCommentCreateSchema = z.object({
|
|
287
|
+
issueId: z.string().min(1),
|
|
288
|
+
body: z.string().min(1),
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
registerWriteTool(
|
|
292
|
+
"linear_comment_create",
|
|
293
|
+
{
|
|
294
|
+
mutates: "linear.comment.create",
|
|
295
|
+
recoverable: true,
|
|
296
|
+
scopeTargetOf: (p) => ({ kind: "issue", value: p.issueId }),
|
|
297
|
+
},
|
|
298
|
+
"Add a comment to a Linear issue.",
|
|
299
|
+
linearCommentCreateSchema,
|
|
300
|
+
async (parsed) => {
|
|
301
|
+
const apiKey = requireProcessEnv("LINEAR_API_KEY");
|
|
302
|
+
const q = `
|
|
303
|
+
mutation CommentCreate($input: CommentCreateInput!) {
|
|
304
|
+
commentCreate(input: $input) {
|
|
305
|
+
success
|
|
306
|
+
comment { id body createdAt }
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
`;
|
|
310
|
+
type Out = {
|
|
311
|
+
commentCreate: { success: boolean; comment: Record<string, unknown> | null };
|
|
312
|
+
};
|
|
313
|
+
const res = await linearGraphql<Out>(apiKey, q, {
|
|
314
|
+
input: { issueId: parsed.issueId, body: parsed.body },
|
|
315
|
+
});
|
|
316
|
+
return jsonResult(linearGqlData(res));
|
|
317
|
+
},
|
|
318
|
+
);
|
|
319
|
+
|
|
320
|
+
const linearFirstOptionalSchema = z.object({
|
|
321
|
+
first: z.number().int().min(1).max(100).optional(),
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
reg("linear_project_list", "List Linear projects.", linearFirstOptionalSchema, async (parsed) => {
|
|
325
|
+
const apiKey = requireProcessEnv("LINEAR_API_KEY");
|
|
326
|
+
const q = `
|
|
327
|
+
query ProjectList($first: Int!) {
|
|
328
|
+
projects(first: $first) {
|
|
329
|
+
nodes {
|
|
330
|
+
id
|
|
331
|
+
name
|
|
332
|
+
description
|
|
333
|
+
slugId
|
|
334
|
+
url
|
|
335
|
+
updatedAt
|
|
336
|
+
state
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
`;
|
|
341
|
+
type Out = { projects: { nodes: ReadonlyArray<Record<string, unknown>> } };
|
|
342
|
+
const res = await linearGraphql<Out>(apiKey, q, { first: parsed.first ?? 50 });
|
|
343
|
+
return jsonResult(linearGqlData(res));
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
const linearProjectIdSchema = z.object({ projectId: z.string().min(1) });
|
|
347
|
+
|
|
348
|
+
reg("linear_project_get", "Get a Linear project by id.", linearProjectIdSchema, async (parsed) => {
|
|
349
|
+
const apiKey = requireProcessEnv("LINEAR_API_KEY");
|
|
350
|
+
const q = `
|
|
351
|
+
query ProjectGet($id: String!) {
|
|
352
|
+
project(id: $id) {
|
|
353
|
+
id
|
|
354
|
+
name
|
|
355
|
+
description
|
|
356
|
+
slugId
|
|
357
|
+
url
|
|
358
|
+
updatedAt
|
|
359
|
+
state
|
|
360
|
+
lead { id displayName }
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
`;
|
|
364
|
+
type Out = { project: Record<string, unknown> | null };
|
|
365
|
+
const res = await linearGraphql<Out>(apiKey, q, { id: parsed.projectId });
|
|
366
|
+
return jsonResult(linearGqlData(res));
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
const linearCycleListSchema = z.object({
|
|
370
|
+
teamId: z.string().min(1),
|
|
371
|
+
first: z.number().int().min(1).max(50).optional(),
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
reg(
|
|
375
|
+
"linear_cycle_list",
|
|
376
|
+
"List Linear cycles for a team.",
|
|
377
|
+
linearCycleListSchema,
|
|
378
|
+
async (parsed) => {
|
|
379
|
+
const apiKey = requireProcessEnv("LINEAR_API_KEY");
|
|
380
|
+
const q = `
|
|
381
|
+
query CycleList($teamId: ID!, $first: Int!) {
|
|
382
|
+
team(id: $teamId) {
|
|
383
|
+
id
|
|
384
|
+
cycles(first: $first) {
|
|
385
|
+
nodes {
|
|
386
|
+
id
|
|
387
|
+
name
|
|
388
|
+
number
|
|
389
|
+
startsAt
|
|
390
|
+
endsAt
|
|
391
|
+
completedAt
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
`;
|
|
397
|
+
type Out = {
|
|
398
|
+
team: {
|
|
399
|
+
id: string;
|
|
400
|
+
cycles: { nodes: ReadonlyArray<Record<string, unknown>> };
|
|
401
|
+
} | null;
|
|
402
|
+
};
|
|
403
|
+
const res = await linearGraphql<Out>(apiKey, q, {
|
|
404
|
+
teamId: parsed.teamId,
|
|
405
|
+
first: parsed.first ?? 20,
|
|
406
|
+
});
|
|
407
|
+
return jsonResult(linearGqlData(res));
|
|
408
|
+
},
|
|
409
|
+
);
|
|
410
|
+
|
|
411
|
+
const linearRoadmapFirstSchema = z.object({
|
|
412
|
+
first: z.number().int().min(1).max(50).optional(),
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
reg(
|
|
416
|
+
"linear_roadmap_list",
|
|
417
|
+
"List Linear initiatives (roadmap).",
|
|
418
|
+
linearRoadmapFirstSchema,
|
|
419
|
+
async (parsed) => {
|
|
420
|
+
const apiKey = requireProcessEnv("LINEAR_API_KEY");
|
|
421
|
+
const q = `
|
|
422
|
+
query InitiativeList($first: Int!) {
|
|
423
|
+
initiatives(first: $first) {
|
|
424
|
+
nodes {
|
|
425
|
+
id
|
|
426
|
+
name
|
|
427
|
+
description
|
|
428
|
+
targetDate
|
|
429
|
+
updatedAt
|
|
430
|
+
url
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
`;
|
|
435
|
+
type Out = { initiatives: { nodes: ReadonlyArray<Record<string, unknown>> } };
|
|
436
|
+
const res = await linearGraphql<Out>(apiKey, q, { first: parsed.first ?? 30 });
|
|
437
|
+
return jsonResult(linearGqlData(res));
|
|
438
|
+
},
|
|
439
|
+
);
|
|
440
|
+
|
|
441
|
+
reg(
|
|
442
|
+
"linear_member_list",
|
|
443
|
+
"List users in the workspace.",
|
|
444
|
+
linearFirstOptionalSchema,
|
|
445
|
+
async (parsed) => {
|
|
446
|
+
const apiKey = requireProcessEnv("LINEAR_API_KEY");
|
|
447
|
+
const q = `
|
|
448
|
+
query Users($first: Int!) {
|
|
449
|
+
users(first: $first) {
|
|
450
|
+
nodes {
|
|
451
|
+
id
|
|
452
|
+
name
|
|
453
|
+
displayName
|
|
454
|
+
email
|
|
455
|
+
active
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
`;
|
|
460
|
+
type Out = { users: { nodes: ReadonlyArray<Record<string, unknown>> } };
|
|
461
|
+
const res = await linearGraphql<Out>(apiKey, q, { first: parsed.first ?? 50 });
|
|
462
|
+
return jsonResult(linearGqlData(res));
|
|
463
|
+
},
|
|
464
|
+
);
|
|
465
|
+
|
|
466
|
+
const transport = new StdioServerTransport();
|
|
467
|
+
await server.connect(transport);
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "com.nimbus.localdb",
|
|
3
|
+
"displayName": "Local DB Schema / Saved Queries",
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"description": "Local DB Schema Indexing connector (Tier-5 local class). Indexes saved SQL queries and schema scripts that local database tools (DBeaver, DataGrip, pgAdmin) keep on disk as `.sql` files, as `localdb:saved_query` items — the SQL TEXT (capped) plus lightweight metadata (relative path, referenced table names, statement count, size). This enables semantic recall of 'that one SQL query I wrote last month' without any live database connection. It is a pure, bounded, path-traversal-guarded filesystem read: NO database is ever connected to, NO query is ever executed, and NO binary is spawned. The user points the connector at the directory their DB tool stores scripts/consoles in via the non-secret path `localdb.scripts_dir`; read tools are `localdb_list`, `localdb_get`, `localdb_search`. The configured scripts directory arrives as the scoped environment variable LOCALDB_SCRIPTS_DIR and is added to the sandbox filesystem.read allow-list at spawn time. No network access.",
|
|
6
|
+
"author": "Nimbus",
|
|
7
|
+
"entrypoint": "dist/server.js",
|
|
8
|
+
"runtime": "bun",
|
|
9
|
+
"permissions": {
|
|
10
|
+
"network": [],
|
|
11
|
+
"filesystem": { "read": [], "write": [] }
|
|
12
|
+
},
|
|
13
|
+
"hitlRequired": [],
|
|
14
|
+
"syncInterval": 600,
|
|
15
|
+
"minNimbusVersion": "0.2.0"
|
|
16
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { readdir, readFile, stat } from "node:fs/promises";
|
|
2
|
+
import { isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Local-only reader for saved SQL queries. This module is the MCP-server-side
|
|
6
|
+
* counterpart of the gateway's `mapLocalDbQueryToItem` mapper: it reads `.sql`
|
|
7
|
+
* files from the configured local DB-tool scripts dir (DBeaver/DataGrip/pgAdmin
|
|
8
|
+
* exports) and returns their TEXT + lightweight metadata for semantic recall.
|
|
9
|
+
*
|
|
10
|
+
* No database is ever connected to and no query is ever executed — this is a
|
|
11
|
+
* pure filesystem read, bounded and path-traversal-guarded.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const MAX_FILES = 2000;
|
|
15
|
+
const MAX_FILE_BYTES = 2 * 1024 * 1024;
|
|
16
|
+
const MAX_WALK_DEPTH = 12;
|
|
17
|
+
const PREVIEW_MAX_CHARS = 4000;
|
|
18
|
+
|
|
19
|
+
export interface SavedQuery {
|
|
20
|
+
/** Path relative to the configured scripts dir — the stable id. */
|
|
21
|
+
readonly relativePath: string;
|
|
22
|
+
readonly title: string;
|
|
23
|
+
readonly sizeBytes: number;
|
|
24
|
+
readonly lineCount: number;
|
|
25
|
+
/** SQL text, capped. */
|
|
26
|
+
readonly preview: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function clamp(s: string, max: number): string {
|
|
30
|
+
return s.length > max ? `${s.slice(0, max)}…` : s;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function baseTitle(relativePath: string): string {
|
|
34
|
+
const segments = relativePath.split(/[/\\]/);
|
|
35
|
+
const last = segments.at(-1) ?? relativePath;
|
|
36
|
+
return last.replace(/\.sql$/i, "");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Resolve the configured scripts dir from the environment. Throws when unset so
|
|
41
|
+
* the MCP tools surface a clear error rather than reading an empty path.
|
|
42
|
+
*/
|
|
43
|
+
export function scriptsDir(): string {
|
|
44
|
+
const dir = process.env["LOCALDB_SCRIPTS_DIR"]?.trim();
|
|
45
|
+
if (dir === undefined || dir === "") {
|
|
46
|
+
throw new Error("LOCALDB_SCRIPTS_DIR is not set");
|
|
47
|
+
}
|
|
48
|
+
return resolve(dir);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Path-traversal guard: confirm `candidate` (an absolute resolved path) is the
|
|
53
|
+
* scripts dir itself or a descendant. Rejects `..`-escaping inputs.
|
|
54
|
+
*/
|
|
55
|
+
export function assertWithinScriptsDir(candidate: string, root: string): void {
|
|
56
|
+
const rel = relative(root, candidate);
|
|
57
|
+
if (rel === "") {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
if (rel.startsWith("..") || rel.startsWith(`..${sep}`) || isAbsolute(rel)) {
|
|
61
|
+
throw new Error("path escapes the configured local DB scripts dir");
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function collectSqlFiles(root: string): Promise<string[]> {
|
|
66
|
+
const found: string[] = [];
|
|
67
|
+
async function walk(dir: string, depth: number): Promise<void> {
|
|
68
|
+
if (depth > MAX_WALK_DEPTH || found.length >= MAX_FILES) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
let entries: import("node:fs").Dirent[];
|
|
72
|
+
try {
|
|
73
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
74
|
+
} catch {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
for (const entry of entries) {
|
|
78
|
+
if (found.length >= MAX_FILES) {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
const full = join(dir, entry.name);
|
|
82
|
+
if (entry.isDirectory()) {
|
|
83
|
+
await walk(full, depth + 1);
|
|
84
|
+
} else if (entry.isFile() && entry.name.toLowerCase().endsWith(".sql")) {
|
|
85
|
+
found.push(full);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
await walk(root, 0);
|
|
90
|
+
return found;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function readSavedQuery(path: string, root: string): Promise<SavedQuery | null> {
|
|
94
|
+
try {
|
|
95
|
+
const buf = await readFile(path);
|
|
96
|
+
if (buf.byteLength > MAX_FILE_BYTES) {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
const sql = buf.toString("utf8");
|
|
100
|
+
if (sql.trim() === "") {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
let sizeBytes = buf.byteLength;
|
|
104
|
+
try {
|
|
105
|
+
const info = await stat(path);
|
|
106
|
+
sizeBytes = Number.isFinite(info.size) ? info.size : buf.byteLength;
|
|
107
|
+
} catch {
|
|
108
|
+
sizeBytes = buf.byteLength;
|
|
109
|
+
}
|
|
110
|
+
const relativePath = relative(root, path);
|
|
111
|
+
return {
|
|
112
|
+
relativePath,
|
|
113
|
+
title: baseTitle(relativePath) || relativePath,
|
|
114
|
+
sizeBytes,
|
|
115
|
+
lineCount: sql.split("\n").length,
|
|
116
|
+
preview: clamp(sql, PREVIEW_MAX_CHARS),
|
|
117
|
+
};
|
|
118
|
+
} catch {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Read all saved `.sql` queries under the configured scripts dir (bounded). */
|
|
124
|
+
export async function scanSavedQueries(): Promise<SavedQuery[]> {
|
|
125
|
+
const root = scriptsDir();
|
|
126
|
+
const files = await collectSqlFiles(root);
|
|
127
|
+
const out: SavedQuery[] = [];
|
|
128
|
+
for (const file of files) {
|
|
129
|
+
assertWithinScriptsDir(file, root);
|
|
130
|
+
const q = await readSavedQuery(file, root);
|
|
131
|
+
if (q !== null) {
|
|
132
|
+
out.push(q);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return out;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Read one saved query by its relative path (path-guarded). */
|
|
139
|
+
export async function getSavedQuery(relativePath: string): Promise<SavedQuery | null> {
|
|
140
|
+
const root = scriptsDir();
|
|
141
|
+
const candidate = resolve(root, relativePath);
|
|
142
|
+
assertWithinScriptsDir(candidate, root);
|
|
143
|
+
return readSavedQuery(candidate, root);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Substring search over saved-query title / relative path / SQL text. */
|
|
147
|
+
export function filterSavedQueries(queries: readonly SavedQuery[], query: string): SavedQuery[] {
|
|
148
|
+
const q = query.trim().toLowerCase();
|
|
149
|
+
if (q === "") {
|
|
150
|
+
return [...queries];
|
|
151
|
+
}
|
|
152
|
+
return queries.filter(
|
|
153
|
+
(sq) =>
|
|
154
|
+
sq.title.toLowerCase().includes(q) ||
|
|
155
|
+
sq.relativePath.toLowerCase().includes(q) ||
|
|
156
|
+
sq.preview.toLowerCase().includes(q),
|
|
157
|
+
);
|
|
158
|
+
}
|