Merge conflicts

This commit is contained in:
timothycarambat 2024-07-23 12:48:25 -07:00
commit 714f88891d
25 changed files with 1048 additions and 27 deletions

View File

@ -6,7 +6,7 @@ concurrency:
on: on:
push: push:
branches: ['1915-docker-perms'] # master branch only. Do not modify. branches: ['-dev'] # put your current branch to create a build. Core team only.
paths-ignore: paths-ignore:
- '**.md' - '**.md'
- 'cloud-deployments/*' - 'cloud-deployments/*'

View File

@ -31,6 +31,7 @@
"Mintplex", "Mintplex",
"moderations", "moderations",
"numpages", "numpages",
"odbc",
"Ollama", "Ollama",
"Oobabooga", "Oobabooga",
"openai", "openai",

View File

@ -1,5 +1,6 @@
const { setDataSigner } = require("../middleware/setDataSigner"); const { setDataSigner } = require("../middleware/setDataSigner");
const { verifyPayloadIntegrity } = require("../middleware/verifyIntegrity"); const { verifyPayloadIntegrity } = require("../middleware/verifyIntegrity");
const { resolveRepoLoader, resolveRepoLoaderFunction } = require("../utils/extensions/RepoLoader");
const { reqBody } = require("../utils/http"); const { reqBody } = require("../utils/http");
const { validURL } = require("../utils/url"); const { validURL } = require("../utils/url");
const RESYNC_METHODS = require("./resync"); const RESYNC_METHODS = require("./resync");
@ -28,15 +29,16 @@ function extensions(app) {
) )
app.post( app.post(
"/ext/github-repo", "/ext/:repo_platform-repo",
[verifyPayloadIntegrity, setDataSigner], [verifyPayloadIntegrity, setDataSigner],
async function (request, response) { async function (request, response) {
try { try {
const { loadGithubRepo } = require("../utils/extensions/GithubRepo"); const loadRepo = resolveRepoLoaderFunction(request.params.repo_platform);
const { success, reason, data } = await loadGithubRepo( const { success, reason, data } = await loadRepo(
reqBody(request), reqBody(request),
response, response,
); );
console.log({ success, reason, data })
response.status(200).json({ response.status(200).json({
success, success,
reason, reason,
@ -56,12 +58,12 @@ function extensions(app) {
// gets all branches for a specific repo // gets all branches for a specific repo
app.post( app.post(
"/ext/github-repo/branches", "/ext/:repo_platform-repo/branches",
[verifyPayloadIntegrity], [verifyPayloadIntegrity],
async function (request, response) { async function (request, response) {
try { try {
const GithubRepoLoader = require("../utils/extensions/GithubRepo/RepoLoader"); const RepoLoader = resolveRepoLoader(request.params.repo_platform);
const allBranches = await new GithubRepoLoader( const allBranches = await new RepoLoader(
reqBody(request) reqBody(request)
).getRepoBranches(); ).getRepoBranches();
response.status(200).json({ response.status(200).json({

View File

@ -86,7 +86,7 @@ async function resyncGithub({ chunkSource }, response) {
// Github file data is `payload` encrypted (might contain PAT). So we need to expand its // Github file data is `payload` encrypted (might contain PAT). So we need to expand its
// encrypted payload back into query params so we can reFetch the page with same access token/params. // encrypted payload back into query params so we can reFetch the page with same access token/params.
const source = response.locals.encryptionWorker.expandPayload(chunkSource); const source = response.locals.encryptionWorker.expandPayload(chunkSource);
const { fetchGithubFile } = require("../../utils/extensions/GithubRepo"); const { fetchGithubFile } = require("../../utils/extensions/RepoLoader/GithubRepo");
const { success, reason, content } = await fetchGithubFile({ const { success, reason, content } = await fetchGithubFile({
repoUrl: `https:${source.pathname}`, // need to add back the real protocol repoUrl: `https:${source.pathname}`, // need to add back the real protocol
branch: source.searchParams.get('branch'), branch: source.searchParams.get('branch'),

View File

@ -32,6 +32,7 @@
"mammoth": "^1.6.0", "mammoth": "^1.6.0",
"mbox-parser": "^1.0.1", "mbox-parser": "^1.0.1",
"mime": "^3.0.0", "mime": "^3.0.0",
"minimatch": "5.1.0",
"moment": "^2.29.4", "moment": "^2.29.4",
"multer": "^1.4.5-lts.1", "multer": "^1.4.5-lts.1",
"node-html-parser": "^6.1.13", "node-html-parser": "^6.1.13",

View File

@ -1,4 +1,21 @@
class RepoLoader { /**
* @typedef {Object} RepoLoaderArgs
* @property {string} repo - The GitHub repository URL.
* @property {string} [branch] - The branch to load from (optional).
* @property {string} [accessToken] - GitHub access token for authentication (optional).
* @property {string[]} [ignorePaths] - Array of paths to ignore when loading (optional).
*/
/**
* @class
* @classdesc Loads and manages GitHub repository content.
*/
class GitHubRepoLoader {
/**
* Creates an instance of RepoLoader.
* @param {RepoLoaderArgs} [args] - The configuration options.
* @returns {GitHubRepoLoader}
*/
constructor(args = {}) { constructor(args = {}) {
this.ready = false; this.ready = false;
this.repo = args?.repo; this.repo = args?.repo;
@ -67,6 +84,10 @@ class RepoLoader {
return; return;
} }
/**
* Initializes the RepoLoader instance.
* @returns {Promise<RepoLoader>} The initialized RepoLoader instance.
*/
async init() { async init() {
if (!this.#validGithubUrl()) return; if (!this.#validGithubUrl()) return;
await this.#validBranch(); await this.#validBranch();
@ -75,6 +96,11 @@ class RepoLoader {
return this; return this;
} }
/**
* Recursively loads the repository content.
* @returns {Promise<Array<Object>>} An array of loaded documents.
* @throws {Error} If the RepoLoader is not in a ready state.
*/
async recursiveLoader() { async recursiveLoader() {
if (!this.ready) throw new Error("[Github Loader]: not in ready state!"); if (!this.ready) throw new Error("[Github Loader]: not in ready state!");
const { const {
@ -109,7 +135,10 @@ class RepoLoader {
}, []); }, []);
} }
// Get all branches for a given repo. /**
* Retrieves all branches for the repository.
* @returns {Promise<string[]>} An array of branch names.
*/
async getRepoBranches() { async getRepoBranches() {
if (!this.#validGithubUrl() || !this.author || !this.project) return []; if (!this.#validGithubUrl() || !this.author || !this.project) return [];
await this.#validateAccessToken(); // Ensure API access token is valid for pre-flight await this.#validateAccessToken(); // Ensure API access token is valid for pre-flight
@ -151,6 +180,11 @@ class RepoLoader {
return this.#branchPrefSort(this.branches); return this.#branchPrefSort(this.branches);
} }
/**
* Fetches the content of a single file from the repository.
* @param {string} sourceFilePath - The path to the file in the repository.
* @returns {Promise<string|null>} The content of the file, or null if fetching fails.
*/
async fetchSingleFile(sourceFilePath) { async fetchSingleFile(sourceFilePath) {
try { try {
return fetch( return fetch(
@ -182,4 +216,4 @@ class RepoLoader {
} }
} }
module.exports = RepoLoader; module.exports = GitHubRepoLoader;

View File

@ -3,8 +3,8 @@ const fs = require("fs");
const path = require("path"); const path = require("path");
const { default: slugify } = require("slugify"); const { default: slugify } = require("slugify");
const { v4 } = require("uuid"); const { v4 } = require("uuid");
const { writeToServerDocuments, documentsFolder } = require("../../files"); const { writeToServerDocuments, documentsFolder } = require("../../../files");
const { tokenizeString } = require("../../tokenizer"); const { tokenizeString } = require("../../../tokenizer");
/** /**
* Load in a Github Repo recursively or just the top level if no PAT is provided * Load in a Github Repo recursively or just the top level if no PAT is provided
@ -37,6 +37,7 @@ async function loadGithubRepo(args, response) {
const outFolder = slugify( const outFolder = slugify(
`${repo.author}-${repo.project}-${repo.branch}-${v4().slice(0, 4)}` `${repo.author}-${repo.project}-${repo.branch}-${v4().slice(0, 4)}`
).toLowerCase(); ).toLowerCase();
const outFolderPath = path.resolve(documentsFolder, outFolder); const outFolderPath = path.resolve(documentsFolder, outFolder);
if (!fs.existsSync(outFolderPath)) if (!fs.existsSync(outFolderPath))
fs.mkdirSync(outFolderPath, { recursive: true }); fs.mkdirSync(outFolderPath, { recursive: true });

View File

@ -0,0 +1,289 @@
const minimatch = require("minimatch");
/**
* @typedef {Object} RepoLoaderArgs
* @property {string} repo - The GitLab repository URL.
* @property {string} [branch] - The branch to load from (optional).
* @property {string} [accessToken] - GitLab access token for authentication (optional).
* @property {string[]} [ignorePaths] - Array of paths to ignore when loading (optional).
*/
/**
* @typedef {Object} FileTreeObject
* @property {string} id - The file object ID.
* @property {string} name - name of file.
* @property {('blob'|'tree')} type - type of file object.
* @property {string} path - path + name of file.
* @property {string} mode - Linux permission code.
*/
/**
* @class
* @classdesc Loads and manages GitLab repository content.
*/
class GitLabRepoLoader {
/**
* Creates an instance of RepoLoader.
* @param {RepoLoaderArgs} [args] - The configuration options.
* @returns {GitLabRepoLoader}
*/
constructor(args = {}) {
this.ready = false;
this.repo = args?.repo;
this.branch = args?.branch;
this.accessToken = args?.accessToken || null;
this.ignorePaths = args?.ignorePaths || [];
this.projectId = null;
this.apiBase = "https://gitlab.com";
this.author = null;
this.project = null;
this.branches = [];
}
#validGitlabUrl() {
const UrlPattern = require("url-pattern");
const validPatterns = [
new UrlPattern("https\\://gitlab.com/(:projectId(*))", {
segmentValueCharset: "a-zA-Z0-9-._~%/+",
}),
// This should even match the regular hosted URL, but we may want to know
// if this was a hosted GitLab (above) or a self-hosted (below) instance
// since the API interface could be different.
new UrlPattern(
"(:protocol(http|https))\\://(:hostname*)/(:projectId(*))",
{
segmentValueCharset: "a-zA-Z0-9-._~%/+",
}
),
];
let match = null;
for (const pattern of validPatterns) {
if (match !== null) continue;
match = pattern.match(this.repo);
}
if (!match) return false;
const [author, project] = match.projectId.split("/");
this.projectId = encodeURIComponent(match.projectId);
this.apiBase = new URL(this.repo).origin;
this.author = author;
this.project = project;
return true;
}
async #validBranch() {
await this.getRepoBranches();
if (!!this.branch && this.branches.includes(this.branch)) return;
console.log(
"[Gitlab Loader]: Branch not set! Auto-assigning to a default branch."
);
this.branch = this.branches.includes("main") ? "main" : "master";
console.log(`[Gitlab Loader]: Branch auto-assigned to ${this.branch}.`);
return;
}
async #validateAccessToken() {
if (!this.accessToken) return;
try {
await fetch(`${this.apiBase}/api/v4/user`, {
method: "GET",
headers: this.accessToken ? { "PRIVATE-TOKEN": this.accessToken } : {},
}).then((res) => res.ok);
} catch (e) {
console.error(
"Invalid Gitlab Access Token provided! Access token will not be used",
e.message
);
this.accessToken = null;
}
}
/**
* Initializes the RepoLoader instance.
* @returns {Promise<RepoLoader>} The initialized RepoLoader instance.
*/
async init() {
if (!this.#validGitlabUrl()) return;
await this.#validBranch();
await this.#validateAccessToken();
this.ready = true;
return this;
}
/**
* Recursively loads the repository content.
* @returns {Promise<Array<Object>>} An array of loaded documents.
* @throws {Error} If the RepoLoader is not in a ready state.
*/
async recursiveLoader() {
if (!this.ready) throw new Error("[Gitlab Loader]: not in ready state!");
if (this.accessToken)
console.log(
`[Gitlab Loader]: Access token set! Recursive loading enabled!`
);
const files = await this.fetchFilesRecursive();
const docs = [];
for (const file of files) {
if (this.ignorePaths.some((path) => file.path.includes(path))) continue;
const content = await this.fetchSingleFileContents(file.path);
if (content) {
docs.push({
pageContent: content,
metadata: { source: file.path },
});
}
}
return docs;
}
#branchPrefSort(branches = []) {
const preferredSort = ["main", "master"];
return branches.reduce((acc, branch) => {
if (preferredSort.includes(branch)) return [branch, ...acc];
return [...acc, branch];
}, []);
}
/**
* Retrieves all branches for the repository.
* @returns {Promise<string[]>} An array of branch names.
*/
async getRepoBranches() {
if (!this.#validGitlabUrl() || !this.projectId) return [];
await this.#validateAccessToken();
try {
this.branches = await fetch(
`${this.apiBase}/api/v4/projects/${this.projectId}/repository/branches`,
{
method: "GET",
headers: {
Accepts: "application/json",
...(this.accessToken ? { "PRIVATE-TOKEN": this.accessToken } : {}),
},
}
)
.then((res) => res.json())
.then((branches) => {
return branches.map((b) => b.name);
})
.catch((e) => {
console.error(e);
return [];
});
return this.#branchPrefSort(this.branches);
} catch (err) {
console.log(`RepoLoader.getRepoBranches`, err);
this.branches = [];
return [];
}
}
/**
* Returns list of all file objects from tree API for GitLab
* @returns {Promise<FileTreeObject[]>}
*/
async fetchFilesRecursive() {
const files = [];
let perPage = 100;
let fetching = true;
let page = 1;
while (fetching) {
try {
const params = new URLSearchParams({
ref: this.branch,
recursive: true,
per_page: perPage,
page,
});
const queryUrl = `${this.apiBase}/api/v4/projects/${
this.projectId
}/repository/tree?${params.toString()}`;
const response = await fetch(queryUrl, {
method: "GET",
headers: this.accessToken
? { "PRIVATE-TOKEN": this.accessToken }
: {},
});
const totalPages = Number(response.headers.get("x-total-pages"));
const nextPage = Number(response.headers.get("x-next-page"));
const data = await response.json();
/** @type {FileTreeObject[]} */
const objects = Array.isArray(data)
? data.filter((item) => item.type === "blob")
: []; // only get files, not paths or submodules
if (objects.length === 0) {
fetching = false;
break;
}
// Apply ignore path rules to found objects. If any rules match it is an invalid file path.
console.log(
`Found ${objects.length} blobs from repo from pg ${page}/${totalPages}`
);
for (const file of objects) {
const isIgnored = this.ignorePaths.some((ignorePattern) =>
minimatch(file.path, ignorePattern, { matchBase: true })
);
if (!isIgnored) files.push(file);
}
if (page === totalPages) {
fetching = false;
break;
}
page = Number(nextPage);
} catch (e) {
console.error(`RepoLoader.getRepositoryTree`, e);
fetching = false;
break;
}
}
return files;
}
/**
* Fetches the content of a single file from the repository.
* @param {string} sourceFilePath - The path to the file in the repository.
* @returns {Promise<string|null>} The content of the file, or null if fetching fails.
*/
async fetchSingleFileContents(sourceFilePath) {
try {
const data = await fetch(
`${this.apiBase}/api/v4/projects/${
this.projectId
}/repository/files/${encodeURIComponent(sourceFilePath)}/raw?ref=${
this.branch
}`,
{
method: "GET",
headers: this.accessToken
? { "PRIVATE-TOKEN": this.accessToken }
: {},
}
).then((res) => {
if (res.ok) return res.text();
throw new Error(`Failed to fetch single file ${sourceFilePath}`);
});
return data;
} catch (e) {
console.error(`RepoLoader.fetchSingleFileContents`, e);
return null;
}
}
}
module.exports = GitLabRepoLoader;

View File

@ -0,0 +1,138 @@
const RepoLoader = require("./RepoLoader");
const fs = require("fs");
const path = require("path");
const { default: slugify } = require("slugify");
const { v4 } = require("uuid");
const { writeToServerDocuments, documentsFolder } = require("../../../files");
const { tokenizeString } = require("../../../tokenizer");
/**
* Load in a Gitlab Repo recursively or just the top level if no PAT is provided
* @param {object} args - forwarded request body params
* @param {import("../../../middleware/setDataSigner").ResponseWithSigner} response - Express response object with encryptionWorker
* @returns
*/
async function loadGitlabRepo(args, response) {
const repo = new RepoLoader(args);
await repo.init();
if (!repo.ready)
return {
success: false,
reason: "Could not prepare Gitlab repo for loading! Check URL",
};
console.log(
`-- Working GitLab ${repo.author}/${repo.project}:${repo.branch} --`
);
const docs = await repo.recursiveLoader();
if (!docs.length) {
return {
success: false,
reason: "No files were found for those settings.",
};
}
console.log(`[GitLab Loader]: Found ${docs.length} source files. Saving...`);
const outFolder = slugify(
`${repo.author}-${repo.project}-${repo.branch}-${v4().slice(0, 4)}`
).toLowerCase();
const outFolderPath = path.resolve(documentsFolder, outFolder);
if (!fs.existsSync(outFolderPath))
fs.mkdirSync(outFolderPath, { recursive: true });
for (const doc of docs) {
if (!doc.pageContent) continue;
const data = {
id: v4(),
url: "gitlab://" + doc.metadata.source,
title: doc.metadata.source,
docAuthor: repo.author,
description: "No description found.",
docSource: doc.metadata.source,
chunkSource: generateChunkSource(
repo,
doc,
response.locals.encryptionWorker
),
published: new Date().toLocaleString(),
wordCount: doc.pageContent.split(" ").length,
pageContent: doc.pageContent,
token_count_estimate: tokenizeString(doc.pageContent).length,
};
console.log(
`[GitLab Loader]: Saving ${doc.metadata.source} to ${outFolder}`
);
writeToServerDocuments(
data,
`${slugify(doc.metadata.source)}-${data.id}`,
outFolderPath
);
}
return {
success: true,
reason: null,
data: {
author: repo.author,
repo: repo.project,
projectId: repo.projectId,
branch: repo.branch,
files: docs.length,
destination: outFolder,
},
};
}
async function fetchGitlabFile({
repoUrl,
branch,
accessToken = null,
sourceFilePath,
}) {
const repo = new RepoLoader({
repo: repoUrl,
branch,
accessToken,
});
await repo.init();
if (!repo.ready)
return {
success: false,
content: null,
reason: "Could not prepare GitLab repo for loading! Check URL or PAT.",
};
console.log(
`-- Working GitLab ${repo.author}/${repo.project}:${repo.branch} file:${sourceFilePath} --`
);
const fileContent = await repo.fetchSingleFile(sourceFilePath);
if (!fileContent) {
return {
success: false,
reason: "Target file returned a null content response.",
content: null,
};
}
return {
success: true,
reason: null,
content: fileContent,
};
}
function generateChunkSource(repo, doc, encryptionWorker) {
const payload = {
projectId: decodeURIComponent(repo.projectId),
branch: repo.branch,
path: doc.metadata.source,
pat: !!repo.accessToken ? repo.accessToken : null,
};
return `gitlab://${repo.repo}?payload=${encryptionWorker.encrypt(
JSON.stringify(payload)
)}`;
}
module.exports = { loadGitlabRepo, fetchGitlabFile };

View File

@ -0,0 +1,41 @@
/**
* Dynamically load the correct repository loader from a specific platform
* by default will return Github.
* @param {('github'|'gitlab')} platform
* @returns {import("./GithubRepo/RepoLoader")|import("./GitlabRepo/RepoLoader")} the repo loader class for provider
*/
function resolveRepoLoader(platform = "github") {
switch (platform) {
case "github":
console.log(`Loading GitHub RepoLoader...`);
return require("./GithubRepo/RepoLoader");
case "gitlab":
console.log(`Loading GitLab RepoLoader...`);
return require("./GitlabRepo/RepoLoader");
default:
console.log(`Loading GitHub RepoLoader...`);
return require("./GithubRepo/RepoLoader");
}
}
/**
* Dynamically load the correct repository loader function from a specific platform
* by default will return Github.
* @param {('github'|'gitlab')} platform
* @returns {import("./GithubRepo")['fetchGithubFile'] | import("./GitlabRepo")['fetchGitlabFile']} the repo loader class for provider
*/
function resolveRepoLoaderFunction(platform = "github") {
switch (platform) {
case "github":
console.log(`Loading GitHub loader function...`);
return require("./GithubRepo").loadGithubRepo;
case "gitlab":
console.log(`Loading GitLab loader function...`);
return require("./GitlabRepo").loadGitlabRepo;
default:
console.log(`Loading GitHub loader function...`);
return require("./GithubRepo").loadGithubRepo;
}
}
module.exports = { resolveRepoLoader, resolveRepoLoaderFunction };

View File

@ -581,6 +581,13 @@ brace-expansion@^1.1.7:
balanced-match "^1.0.0" balanced-match "^1.0.0"
concat-map "0.0.1" concat-map "0.0.1"
brace-expansion@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae"
integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==
dependencies:
balanced-match "^1.0.0"
braces@~3.0.2: braces@~3.0.2:
version "3.0.2" version "3.0.2"
resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107"
@ -2226,6 +2233,13 @@ mimic-response@^3.1.0:
resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-3.1.0.tgz#2d1d59af9c1b129815accc2c46a022a5ce1fa3c9" resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-3.1.0.tgz#2d1d59af9c1b129815accc2c46a022a5ce1fa3c9"
integrity sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ== integrity sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==
minimatch@5.1.0:
version "5.1.0"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.0.tgz#1717b464f4971b144f6aabe8f2d0b8e4511e09c7"
integrity sha512-9TPBGGak4nHfGZsPBohm9AWg6NoT7QTCehS3BIJABslyZbzxfV78QM2Y6+i741OPZIafFAaiiEMh5OyIrJPgtg==
dependencies:
brace-expansion "^2.0.1"
minimatch@^3.1.1, minimatch@^3.1.2: minimatch@^3.1.1, minimatch@^3.1.2:
version "3.1.2" version "3.1.2"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b"

View File

@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 380 380">
<rect width="380" height="380" fill="#FFFFFF"/>
<path fill="#e24329" d="M282.83,170.73l-.27-.69-26.14-68.22a6.81,6.81,0,0,0-2.69-3.24,7,7,0,0,0-8,.43,7,7,0,0,0-2.32,3.52l-17.65,54H154.29l-17.65-54A6.86,6.86,0,0,0,134.32,99a7,7,0,0,0-8-.43,6.87,6.87,0,0,0-2.69,3.24L97.44,170l-.26.69a48.54,48.54,0,0,0,16.1,56.1l.09.07.24.17,39.82,29.82,19.7,14.91,12,9.06a8.07,8.07,0,0,0,9.76,0l12-9.06,19.7-14.91,40.06-30,.1-.08A48.56,48.56,0,0,0,282.83,170.73Z"/>
<path fill="#fc6d26" d="M282.83,170.73l-.27-.69a88.3,88.3,0,0,0-35.15,15.8L190,229.25c19.55,14.79,36.57,27.64,36.57,27.64l40.06-30,.1-.08A48.56,48.56,0,0,0,282.83,170.73Z"/>
<path fill="#fca326" d="M153.43,256.89l19.7,14.91,12,9.06a8.07,8.07,0,0,0,9.76,0l12-9.06,19.7-14.91S209.55,244,190,229.25C170.45,244,153.43,256.89,153.43,256.89Z"/>
<path fill="#fc6d26" d="M132.58,185.84A88.19,88.19,0,0,0,97.44,170l-.26.69a48.54,48.54,0,0,0,16.1,56.1l.09.07.24.17,39.82,29.82s17-12.85,36.57-27.64Z"/>
</svg>

After

Width:  |  Height:  |  Size: 1021 B

View File

@ -1,10 +1,12 @@
import Github from "./github.svg"; import Github from "./github.svg";
import Gitlab from "./gitlab.svg";
import YouTube from "./youtube.svg"; import YouTube from "./youtube.svg";
import Link from "./link.svg"; import Link from "./link.svg";
import Confluence from "./confluence.jpeg"; import Confluence from "./confluence.jpeg";
const ConnectorImages = { const ConnectorImages = {
github: Github, github: Github,
gitlab: Gitlab,
youtube: YouTube, youtube: YouTube,
websiteDepth: Link, websiteDepth: Link,
confluence: Confluence, confluence: Confluence,

View File

@ -0,0 +1,310 @@
import React, { useEffect, useState } from "react";
import System from "@/models/system";
import showToast from "@/utils/toast";
import pluralize from "pluralize";
import { TagsInput } from "react-tag-input-component";
import { Info, Warning } from "@phosphor-icons/react";
import { Tooltip } from "react-tooltip";
const DEFAULT_BRANCHES = ["main", "master"];
export default function GitlabOptions() {
const [loading, setLoading] = useState(false);
const [repo, setRepo] = useState(null);
const [accessToken, setAccessToken] = useState(null);
const [ignores, setIgnores] = useState([]);
const [settings, setSettings] = useState({
repo: null,
accessToken: null,
});
const handleSubmit = async (e) => {
e.preventDefault();
const form = new FormData(e.target);
try {
setLoading(true);
showToast(
`Fetching all files for repo ${repo} - this may take a while.`,
"info",
{ clear: true, autoClose: false }
);
const { data, error } = await System.dataConnectors.gitlab.collect({
repo: form.get("repo"),
accessToken: form.get("accessToken"),
branch: form.get("branch"),
ignorePaths: ignores,
});
if (!!error) {
showToast(error, "error", { clear: true });
setLoading(false);
return;
}
showToast(
`${data.files} ${pluralize("file", data.files)} collected from ${
data.author
}/${data.repo}:${data.branch}. Output folder is ${data.destination}.`,
"success",
{ clear: true }
);
e.target.reset();
setLoading(false);
return;
} catch (e) {
console.error(e);
showToast(e.message, "error", { clear: true });
setLoading(false);
}
};
return (
<div className="flex w-full">
<div className="flex flex-col w-full px-1 md:pb-6 pb-16">
<form className="w-full" onSubmit={handleSubmit}>
<div className="w-full flex flex-col py-2">
<div className="w-full flex flex-col gap-4">
<div className="flex flex-col pr-10">
<div className="flex flex-col gap-y-1 mb-4">
<label className="text-white text-sm font-bold">
GitLab Repo URL
</label>
<p className="text-xs font-normal text-white/50">
URL of the GitLab repo you wish to collect.
</p>
</div>
<input
type="url"
name="repo"
className="border-none bg-zinc-900 text-white placeholder:text-white/20 text-sm rounded-lg focus:border-white block w-full p-2.5"
placeholder="https://gitlab.com/gitlab-org/gitlab"
required={true}
autoComplete="off"
onChange={(e) => setRepo(e.target.value)}
onBlur={() => setSettings({ ...settings, repo })}
spellCheck={false}
rows={2}
/>
</div>
<div className="flex flex-col pr-10">
<div className="flex flex-col gap-y-1 mb-4">
<label className="text-white font-bold text-sm flex gap-x-2 items-center">
<p className="font-bold text-white">GitLab Access Token</p>{" "}
<p className="text-xs text-white/50 font-light flex items-center">
optional
<PATTooltip accessToken={accessToken} />
</p>
</label>
<p className="text-xs font-normal text-white/50">
Access Token to prevent rate limiting.
</p>
</div>
<input
type="text"
name="accessToken"
className="border-none bg-zinc-900 text-white placeholder:text-white/20 text-sm rounded-lg focus:border-white block w-full p-2.5"
placeholder="glpat-XXXXXXXXXXXXXXXXXXXX"
required={false}
autoComplete="off"
spellCheck={false}
onChange={(e) => setAccessToken(e.target.value)}
onBlur={() => setSettings({ ...settings, accessToken })}
/>
</div>
<GitLabBranchSelection
repo={settings.repo}
accessToken={settings.accessToken}
/>
</div>
<div className="flex flex-col w-full py-4 pr-10">
<div className="flex flex-col gap-y-1 mb-4">
<label className="text-white text-sm flex gap-x-2 items-center">
<p className="text-white text-sm font-bold">File Ignores</p>
</label>
<p className="text-xs font-normal text-white/50">
List in .gitignore format to ignore specific files during
collection. Press enter after each entry you want to save.
</p>
</div>
<TagsInput
value={ignores}
onChange={setIgnores}
name="ignores"
placeholder="!*.js, images/*, .DS_Store, bin/*"
classNames={{
tag: "bg-blue-300/10 text-zinc-800",
input:
"flex bg-zinc-900 text-white placeholder:text-white/20 text-sm rounded-lg focus:border-white",
}}
/>
</div>
</div>
<div className="flex flex-col gap-y-2 w-full pr-10">
<PATAlert accessToken={accessToken} />
<button
type="submit"
disabled={loading}
className="mt-2 w-full justify-center border border-slate-200 px-4 py-2 rounded-lg text-dark-text text-sm font-bold items-center flex gap-x-2 bg-slate-200 hover:bg-slate-300 hover:text-slate-800 disabled:bg-slate-300 disabled:cursor-not-allowed"
>
{loading ? "Collecting files..." : "Submit"}
</button>
{loading && (
<p className="text-xs text-white/50">
Once complete, all files will be available for embedding into
workspaces in the document picker.
</p>
)}
</div>
</form>
</div>
</div>
);
}
function GitLabBranchSelection({ repo, accessToken }) {
const [allBranches, setAllBranches] = useState(DEFAULT_BRANCHES);
const [loading, setLoading] = useState(true);
useEffect(() => {
async function fetchAllBranches() {
if (!repo) {
setAllBranches(DEFAULT_BRANCHES);
setLoading(false);
return;
}
setLoading(true);
const { branches } = await System.dataConnectors.gitlab.branches({
repo,
accessToken,
});
setAllBranches(branches.length > 0 ? branches : DEFAULT_BRANCHES);
setLoading(false);
}
fetchAllBranches();
}, [repo, accessToken]);
if (loading) {
return (
<div className="flex flex-col w-60">
<div className="flex flex-col gap-y-1 mb-4">
<label className="text-white text-sm font-bold">Branch</label>
<p className="text-xs font-normal text-white/50">
Branch you wish to collect files from.
</p>
</div>
<select
name="branch"
required={true}
className="bg-zinc-900 border-gray-500 text-white text-sm rounded-lg block w-full p-2.5"
>
<option disabled={true} selected={true}>
-- loading available branches --
</option>
</select>
</div>
);
}
return (
<div className="flex flex-col w-60">
<div className="flex flex-col gap-y-1 mb-4">
<label className="text-white text-sm font-bold">Branch</label>
<p className="text-xs font-normal text-white/50">
Branch you wish to collect files from.
</p>
</div>
<select
name="branch"
required={true}
className="bg-zinc-900 border-gray-500 text-white text-sm rounded-lg block w-full p-2.5"
>
{allBranches.map((branch) => {
return (
<option key={branch} value={branch}>
{branch}
</option>
);
})}
</select>
</div>
);
}
function PATAlert({ accessToken }) {
if (!!accessToken) return null;
return (
<div className="flex flex-col md:flex-row md:items-center gap-x-2 text-white mb-4 bg-blue-800/30 w-fit rounded-lg px-4 py-2">
<div className="gap-x-2 flex items-center">
<Info className="shrink-0" size={25} />
<p className="text-sm">
Without filling out the <b>GitLab Access Token</b> this data connector
will only be able to collect the <b>top-level</b> files of the repo
due to GitLab's public API rate-limits.
<br />
<br />
<a
href="https://gitlab.com/-/profile/personal_access_tokens"
rel="noreferrer"
target="_blank"
className="underline"
onClick={(e) => e.stopPropagation()}
>
{" "}
Get a free Personal Access Token with a GitLab account here.
</a>
</p>
</div>
</div>
);
}
function PATTooltip({ accessToken }) {
if (!!accessToken) return null;
return (
<>
{!accessToken && (
<Warning
size={14}
className="ml-1 text-orange-500 cursor-pointer"
data-tooltip-id="access-token-tooltip"
data-tooltip-place="right"
/>
)}
<Tooltip
delayHide={300}
id="access-token-tooltip"
className="max-w-xs"
clickable={true}
>
<p className="text-sm">
Without a{" "}
<a
href="https://docs.gitlab.com/ee/user/profile/personal_access_tokens.html"
rel="noreferrer"
target="_blank"
className="underline"
onClick={(e) => e.stopPropagation()}
>
Personal Access Token
</a>
, the GitLab API may limit the number of files that can be collected
due to rate limits. You can{" "}
<a
href="https://gitlab.com/-/profile/personal_access_tokens"
rel="noreferrer"
target="_blank"
className="underline"
onClick={(e) => e.stopPropagation()}
>
create a temporary Access Token
</a>{" "}
to avoid this issue.
</p>
</Tooltip>
</>
);
}

View File

@ -1,6 +1,7 @@
import ConnectorImages from "@/components/DataConnectorOption/media"; import ConnectorImages from "@/components/DataConnectorOption/media";
import { MagnifyingGlass } from "@phosphor-icons/react"; import { MagnifyingGlass } from "@phosphor-icons/react";
import GithubOptions from "./Connectors/Github"; import GithubOptions from "./Connectors/Github";
import GitlabOptions from "./Connectors/Gitlab";
import YoutubeOptions from "./Connectors/Youtube"; import YoutubeOptions from "./Connectors/Youtube";
import ConfluenceOptions from "./Connectors/Confluence"; import ConfluenceOptions from "./Connectors/Confluence";
import { useState } from "react"; import { useState } from "react";
@ -15,6 +16,13 @@ export const DATA_CONNECTORS = {
"Import an entire public or private Github repository in a single click.", "Import an entire public or private Github repository in a single click.",
options: <GithubOptions />, options: <GithubOptions />,
}, },
gitlab: {
name: "GitLab Repo",
image: ConnectorImages.gitlab,
description:
"Import an entire public or private GitLab repository in a single click.",
options: <GitlabOptions />,
},
"youtube-transcript": { "youtube-transcript": {
name: "YouTube Transcript", name: "YouTube Transcript",
image: ConnectorImages.youtube, image: ConnectorImages.youtube,

View File

@ -42,6 +42,45 @@ const DataConnector = {
}); });
}, },
}, },
gitlab: {
branches: async ({ repo, accessToken }) => {
return await fetch(`${API_BASE}/ext/gitlab/branches`, {
method: "POST",
headers: baseHeaders(),
cache: "force-cache",
body: JSON.stringify({ repo, accessToken }),
})
.then((res) => res.json())
.then((res) => {
if (!res.success) throw new Error(res.reason);
return res.data;
})
.then((data) => {
return { branches: data?.branches || [], error: null };
})
.catch((e) => {
console.error(e);
showToast(e.message, "error");
return { branches: [], error: e.message };
});
},
collect: async function ({ repo, accessToken, branch, ignorePaths = [] }) {
return await fetch(`${API_BASE}/ext/gitlab/repo`, {
method: "POST",
headers: baseHeaders(),
body: JSON.stringify({ repo, accessToken, branch, ignorePaths }),
})
.then((res) => res.json())
.then((res) => {
if (!res.success) throw new Error(res.reason);
return { data: res.data, error: null };
})
.catch((e) => {
console.error(e);
return { data: null, error: e.message };
});
},
},
youtube: { youtube: {
transcribe: async ({ url }) => { transcribe: async ({ url }) => {
return await fetch(`${API_BASE}/ext/youtube/transcript`, { return await fetch(`${API_BASE}/ext/youtube/transcript`, {

View File

@ -1,12 +1,14 @@
import PostgreSQLLogo from "./icons/postgresql.png"; import PostgreSQLLogo from "./icons/postgresql.png";
import MySQLLogo from "./icons/mysql.png"; import MySQLLogo from "./icons/mysql.png";
import MSSQLLogo from "./icons/mssql.png"; import MSSQLLogo from "./icons/mssql.png";
import ODBCLogo from "./icons/odbc.png";
import { X } from "@phosphor-icons/react"; import { X } from "@phosphor-icons/react";
export const DB_LOGOS = { export const DB_LOGOS = {
postgresql: PostgreSQLLogo, postgresql: PostgreSQLLogo,
mysql: MySQLLogo, mysql: MySQLLogo,
"sql-server": MSSQLLogo, "sql-server": MSSQLLogo,
odbc: ODBCLogo,
}; };
export default function DBConnection({ connection, onRemove, setHasChanges }) { export default function DBConnection({ connection, onRemove, setHasChanges }) {

View File

@ -11,6 +11,7 @@ function assembleConnectionString({
host = "", host = "",
port = "", port = "",
database = "", database = "",
driver = "",
}) { }) {
if ([username, password, host, database].every((i) => !!i) === false) if ([username, password, host, database].every((i) => !!i) === false)
return `Please fill out all the fields above.`; return `Please fill out all the fields above.`;
@ -21,6 +22,9 @@ function assembleConnectionString({
return `mysql://${username}:${password}@${host}:${port}/${database}`; return `mysql://${username}:${password}@${host}:${port}/${database}`;
case "sql-server": case "sql-server":
return `mssql://${username}:${password}@${host}:${port}/${database}`; return `mssql://${username}:${password}@${host}:${port}/${database}`;
case "odbc":
if (!driver) return `Please fill out the driver field.`;
return `Driver={${driver}};Server=${host};Port=${port};Database=${database};UID=${username};PWD=${password}`;
default: default:
return null; return null;
} }
@ -33,6 +37,7 @@ const DEFAULT_CONFIG = {
host: null, host: null,
port: null, port: null,
database: null, database: null,
driver: null,
}; };
export default function NewSQLConnection({ isOpen, closeModal, onSubmit }) { export default function NewSQLConnection({ isOpen, closeModal, onSubmit }) {
@ -48,12 +53,14 @@ export default function NewSQLConnection({ isOpen, closeModal, onSubmit }) {
function onFormChange() { function onFormChange() {
const form = new FormData(document.getElementById("sql-connection-form")); const form = new FormData(document.getElementById("sql-connection-form"));
setConfig({ setConfig({
username: form.get("username").trim(), username: form.get("username").trim(),
password: form.get("password"), password: form.get("password"),
host: form.get("host").trim(), host: form.get("host").trim(),
port: form.get("port").trim(), port: form.get("port").trim(),
database: form.get("database").trim(), database: form.get("database").trim(),
driver: form.get("driver")?.trim(),
}); });
} }
@ -74,7 +81,7 @@ export default function NewSQLConnection({ isOpen, closeModal, onSubmit }) {
// to the parent container form so we don't have nested forms. // to the parent container form so we don't have nested forms.
return createPortal( return createPortal(
<ModalWrapper isOpen={isOpen}> <ModalWrapper isOpen={isOpen}>
<div className="relative w-full md:w-1/3 max-w-2xl max-h-full md:mt-8"> <div className="relative w-full md:w-fit max-w-2xl max-h-full md:mt-8">
<div className="relative bg-main-gradient rounded-xl shadow-[0_4px_14px_rgba(0,0,0,0.25)] max-h-[85vh] overflow-y-scroll no-scroll"> <div className="relative bg-main-gradient rounded-xl shadow-[0_4px_14px_rgba(0,0,0,0.25)] max-h-[85vh] overflow-y-scroll no-scroll">
<div className="flex items-start justify-between p-4 border-b rounded-t border-gray-500/50"> <div className="flex items-start justify-between p-4 border-b rounded-t border-gray-500/50">
<h3 className="text-xl font-semibold text-white"> <h3 className="text-xl font-semibold text-white">
@ -114,7 +121,7 @@ export default function NewSQLConnection({ isOpen, closeModal, onSubmit }) {
<label className="text-white text-sm font-semibold block my-4"> <label className="text-white text-sm font-semibold block my-4">
Select your SQL engine Select your SQL engine
</label> </label>
<div className="grid md:grid-cols-4 gap-4 grid-cols-2"> <div className="flex flex-wrap gap-x-4 gap-y-4">
<DBEngine <DBEngine
provider="postgresql" provider="postgresql"
active={engine === "postgresql"} active={engine === "postgresql"}
@ -130,6 +137,11 @@ export default function NewSQLConnection({ isOpen, closeModal, onSubmit }) {
active={engine === "sql-server"} active={engine === "sql-server"}
onClick={() => setEngine("sql-server")} onClick={() => setEngine("sql-server")}
/> />
<DBEngine
provider="odbc"
active={engine === "odbc"}
onClick={() => setEngine("odbc")}
/>
</div> </div>
</div> </div>
@ -224,6 +236,23 @@ export default function NewSQLConnection({ isOpen, closeModal, onSubmit }) {
spellCheck={false} spellCheck={false}
/> />
</div> </div>
{engine === "odbc" && (
<div className="flex flex-col">
<label className="text-white text-sm font-semibold block mb-3">
Driver
</label>
<input
type="text"
name="driver"
className="border-none bg-zinc-900 text-white placeholder:text-white/20 text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5"
placeholder="the driver to use eg: MongoDB ODBC 1.2.0 ANSI Driver"
required={true}
autoComplete="off"
spellCheck={false}
/>
</div>
)}
<p className="text-white/40 text-sm"> <p className="text-white/40 text-sm">
{assembleConnectionString({ engine, ...config })} {assembleConnectionString({ engine, ...config })}
</p> </p>

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

View File

@ -5,18 +5,26 @@ const {
ROLES, ROLES,
} = require("../../utils/middleware/multiUserProtected"); } = require("../../utils/middleware/multiUserProtected");
const { validatedRequest } = require("../../utils/middleware/validatedRequest"); const { validatedRequest } = require("../../utils/middleware/validatedRequest");
const {
isSupportedRepoProvider,
} = require("../../utils/middleware/isSupportedRepoProviders");
function extensionEndpoints(app) { function extensionEndpoints(app) {
if (!app) return; if (!app) return;
app.post( app.post(
"/ext/github/branches", "/ext/:repo_platform/branches",
[validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])], [
validatedRequest,
flexUserRoleValid([ROLES.admin, ROLES.manager]),
isSupportedRepoProvider,
],
async (request, response) => { async (request, response) => {
try { try {
const { repo_platform } = request.params;
const responseFromProcessor = const responseFromProcessor =
await new CollectorApi().forwardExtensionRequest({ await new CollectorApi().forwardExtensionRequest({
endpoint: "/ext/github-repo/branches", endpoint: `/ext/${repo_platform}-repo/branches`,
method: "POST", method: "POST",
body: request.body, body: request.body,
}); });
@ -29,18 +37,23 @@ function extensionEndpoints(app) {
); );
app.post( app.post(
"/ext/github/repo", "/ext/:repo_platform/repo",
[validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])], [
validatedRequest,
flexUserRoleValid([ROLES.admin, ROLES.manager]),
isSupportedRepoProvider,
],
async (request, response) => { async (request, response) => {
try { try {
const { repo_platform } = request.params;
const responseFromProcessor = const responseFromProcessor =
await new CollectorApi().forwardExtensionRequest({ await new CollectorApi().forwardExtensionRequest({
endpoint: "/ext/github-repo", endpoint: `/ext/${repo_platform}-repo`,
method: "POST", method: "POST",
body: request.body, body: request.body,
}); });
await Telemetry.sendTelemetry("extension_invoked", { await Telemetry.sendTelemetry("extension_invoked", {
type: "github_repo", type: `${repo_platform}_repo`,
}); });
response.status(200).json(responseFromProcessor); response.status(200).json(responseFromProcessor);
} catch (e) { } catch (e) {

View File

@ -66,6 +66,7 @@
"mysql2": "^3.9.8", "mysql2": "^3.9.8",
"node-html-markdown": "^1.3.0", "node-html-markdown": "^1.3.0",
"node-llama-cpp": "^2.8.0", "node-llama-cpp": "^2.8.0",
"odbc": "^2.4.8",
"ollama": "^0.5.0", "ollama": "^0.5.0",
"openai": "4.38.5", "openai": "4.38.5",
"pg": "^8.11.5", "pg": "^8.11.5",

View File

@ -0,0 +1,60 @@
const odbc = require("odbc");
const UrlPattern = require("url-pattern");
class ODBCConnector {
#connected = false;
database_id = "";
constructor(
config = {
connectionString: null,
}
) {
this.connectionString = config.connectionString;
this._client = null;
this.database_id = this.#parseDatabase();
}
#parseDatabase() {
const regex = /Database=([^;]+)/;
const match = this.connectionString.match(regex);
return match ? match[1] : null;
}
async connect() {
this._client = await odbc.connect(this.connectionString);
this.#connected = true;
return this._client;
}
/**
*
* @param {string} queryString the SQL query to be run
* @returns {import(".").QueryResult}
*/
async runQuery(queryString = "") {
const result = { rows: [], count: 0, error: null };
try {
if (!this.#connected) await this.connect();
const query = await this._client.query(queryString);
result.rows = query;
result.count = query.length;
} catch (err) {
console.log(this.constructor.name, err);
result.error = err.message;
} finally {
await this._client.close();
this.#connected = false;
}
return result;
}
getTablesSql() {
return `SELECT table_name FROM information_schema.tables WHERE table_schema = '${this.database_id}'`;
}
getTableSchemaSql(table_name) {
return `SHOW COLUMNS FROM ${this.database_id}.${table_name};`;
}
}
module.exports.ODBCConnector = ODBCConnector;

View File

@ -2,7 +2,7 @@ const { SystemSettings } = require("../../../../../../models/systemSettings");
const { safeJsonParse } = require("../../../../../http"); const { safeJsonParse } = require("../../../../../http");
/** /**
* @typedef {('postgresql'|'mysql'|'sql-server')} SQLEngine * @typedef {('postgresql'|'mysql'|'sql-server'|'odbc')} SQLEngine
*/ */
/** /**
@ -36,6 +36,9 @@ function getDBClient(identifier = "", connectionConfig = {}) {
case "sql-server": case "sql-server":
const { MSSQLConnector } = require("./MSSQL"); const { MSSQLConnector } = require("./MSSQL");
return new MSSQLConnector(connectionConfig); return new MSSQLConnector(connectionConfig);
case "odbc":
const { ODBCConnector } = require("./ODBC");
return new ODBCConnector(connectionConfig);
default: default:
throw new Error( throw new Error(
`There is no supported database connector for ${identifier}` `There is no supported database connector for ${identifier}`

View File

@ -0,0 +1,12 @@
// Middleware to validate that a repo provider URL is supported.
const REPO_PLATFORMS = ["github", "gitlab"];
function isSupportedRepoProvider(request, response, next) {
const { repo_platform = null } = request.params;
if (!repo_platform || !REPO_PLATFORMS.includes(repo_platform))
return response
.status(500)
.text(`Unsupported repo platform ${repo_platform}`);
next();
}
module.exports = { isSupportedRepoProvider };

View File

@ -673,7 +673,7 @@
"@langchain/core" "~0.1" "@langchain/core" "~0.1"
js-tiktoken "^1.0.11" js-tiktoken "^1.0.11"
"@mapbox/node-pre-gyp@^1.0.11": "@mapbox/node-pre-gyp@^1.0.11", "@mapbox/node-pre-gyp@^1.0.5":
version "1.0.11" version "1.0.11"
resolved "https://registry.yarnpkg.com/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz#417db42b7f5323d79e93b34a6d7a2a12c0df43fa" resolved "https://registry.yarnpkg.com/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz#417db42b7f5323d79e93b34a6d7a2a12c0df43fa"
integrity sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ== integrity sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==
@ -1588,7 +1588,7 @@ arrify@^2.0.0:
resolved "https://registry.yarnpkg.com/arrify/-/arrify-2.0.1.tgz#c9655e9331e0abcd588d2a7cad7e9956f66701fa" resolved "https://registry.yarnpkg.com/arrify/-/arrify-2.0.1.tgz#c9655e9331e0abcd588d2a7cad7e9956f66701fa"
integrity sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug== integrity sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==
async@^3.2.3, async@^3.2.4: async@^3.0.1, async@^3.2.3, async@^3.2.4:
version "3.2.5" version "3.2.5"
resolved "https://registry.yarnpkg.com/async/-/async-3.2.5.tgz#ebd52a8fdaf7a2289a24df399f8d8485c8a46b66" resolved "https://registry.yarnpkg.com/async/-/async-3.2.5.tgz#ebd52a8fdaf7a2289a24df399f8d8485c8a46b66"
integrity sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg== integrity sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==
@ -4813,6 +4813,11 @@ node-abort-controller@^3.1.1:
resolved "https://registry.yarnpkg.com/node-abort-controller/-/node-abort-controller-3.1.1.tgz#a94377e964a9a37ac3976d848cb5c765833b8548" resolved "https://registry.yarnpkg.com/node-abort-controller/-/node-abort-controller-3.1.1.tgz#a94377e964a9a37ac3976d848cb5c765833b8548"
integrity sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ== integrity sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==
node-addon-api@^3.0.2:
version "3.2.1"
resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-3.2.1.tgz#81325e0a2117789c0128dab65e7e38f07ceba161"
integrity sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==
node-addon-api@^5.0.0: node-addon-api@^5.0.0:
version "5.1.0" version "5.1.0"
resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-5.1.0.tgz#49da1ca055e109a23d537e9de43c09cca21eb762" resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-5.1.0.tgz#49da1ca055e109a23d537e9de43c09cca21eb762"
@ -5065,6 +5070,15 @@ octokit@^3.1.0:
"@octokit/request-error" "^5.0.0" "@octokit/request-error" "^5.0.0"
"@octokit/types" "^12.0.0" "@octokit/types" "^12.0.0"
odbc@^2.4.8:
version "2.4.8"
resolved "https://registry.yarnpkg.com/odbc/-/odbc-2.4.8.tgz#56e34a1cafbaf1c2c53eec229b3a7604f890e3bf"
integrity sha512-W4VkBcr8iSe8hqpp2GoFPybCAJefC7eK837XThJkYCW4tBzyQisqkciwt1UYidU1OpKy1589y9dMN0tStiVB1Q==
dependencies:
"@mapbox/node-pre-gyp" "^1.0.5"
async "^3.0.1"
node-addon-api "^3.0.2"
ollama@^0.5.0: ollama@^0.5.0:
version "0.5.0" version "0.5.0"
resolved "https://registry.yarnpkg.com/ollama/-/ollama-0.5.0.tgz#cb9bc709d4d3278c9f484f751b0d9b98b06f4859" resolved "https://registry.yarnpkg.com/ollama/-/ollama-0.5.0.tgz#cb9bc709d4d3278c9f484f751b0d9b98b06f4859"