Spaces:
Running
Running
/** | |
* Copyright (c) 2023 MERCENARIES.AI PTE. LTD. | |
* All rights reserved. | |
*/ | |
// Import required modules | |
import { rmSync } from 'fs' | |
import { ensureDir } from 'fs-extra' | |
import fs from 'fs/promises' | |
import yaml from 'js-yaml' | |
import path from 'path' | |
import { simpleGit } from 'simple-git' | |
import { exec } from 'child_process' | |
const TEMPLATE_EXTENSION_REPO = 'https://github.com/omnitool-ai/omni-extension-template.git' | |
// Function to validate directory existence | |
async function validateDirectoryExists (path) { | |
try { | |
const stats = await fs.stat(path) | |
return stats.isDirectory() // Returns true if directory exists | |
} catch { | |
return false // Returns false if directory doesn't exist | |
} | |
} | |
// Function to validate file existence | |
async function validateFileExists (path) { | |
try { | |
const stats = await fs.stat(path) | |
return stats.isFile() // Returns true if file exists | |
} catch { | |
return false // Returns false if file doesn't exist | |
} | |
} | |
// Function to check if there's a build entry in package.json | |
async function hasBuildScript(packageJsonPath) { | |
try { | |
const content = await fs.readFile(packageJsonPath, 'utf8'); | |
const packageJson = JSON.parse(content); | |
return packageJson.scripts && packageJson.scripts.build; | |
} catch (e) { | |
return false; | |
} | |
} | |
async function try_exec_command(command, cwd) { | |
let cmd_error = null; | |
try | |
{ | |
exec(command, { cwd: cwd }, (error, stdout, stderr) => { | |
if (error) | |
{ | |
cmd_error = `Warning: problem while issuing command ${command} at ${cwd} with stdout: ${stdout}\nstderr: ${stderr}\nerror: ${error}`; | |
} | |
}); | |
} | |
catch (e) | |
{ | |
cmd_error = `Warning: problem while issuing command ${command} at ${cwd} with error message: ${e.message}`; | |
} | |
if (cmd_error) | |
{ | |
console.warn(cmd_error); | |
return false; | |
} | |
return true; | |
} | |
async function getKnownExtensions() { | |
const knownFilePath = path.join(process.cwd(), 'config.default', 'extensions', 'known_extensions.yaml') | |
const knownFileContents = await fs.readFile(knownFilePath, 'utf8') | |
return yaml.load(knownFileContents) | |
} | |
// Function to update extensions | |
const updateExtension = async function (ctx, extensionId) { | |
const sessionId = ctx.session.sessionId | |
const target = path.join(process.cwd(), '/extensions/', extensionId) | |
// Log the target of the update | |
ctx.integration.info('Update', extensionId, target) | |
// Check if the extension and its directory exist | |
if (ctx.app.extensions.has(extensionId) && await validateDirectoryExists(target)) { | |
// Check if the required file is present | |
if (!await validateFileExists(path.join(target, 'extension.yaml'))) { | |
const error = `Failed to update extension:\n${extensionId} appears to not be a valid extension (extension.yaml not found).` | |
ctx.app.sendErrorToSession(sessionId, error) | |
return { error } | |
} | |
// Check if .git directory exists | |
if (await validateDirectoryExists(path.join(target, '.git'))) { | |
await ctx.app.sendMessageToSession(sessionId, `Attempting to update extension: ${extensionId}`, 'text/plain') | |
try { | |
// Update the git repository | |
const git = simpleGit({ baseDir: target }) | |
const result = await git.pull() | |
// Send successful update message | |
await ctx.app.sendMessageToSession(sessionId, 'Extension updated (May require a server restart).\n', | |
'text/plain', | |
{ | |
objects: [result], | |
commands: [ | |
{ | |
title: 'Restart Server', | |
id: 'rs', | |
args: [] | |
} | |
] | |
} | |
) | |
return true | |
} catch (e) { | |
// Handle update failure | |
const error = `Failed to update extension:\n${e.message}` | |
ctx.integration.error(error) | |
ctx.app.sendErrorToSession(sessionId, error) | |
return { error } | |
} | |
} else { | |
// Handle missing git repository case | |
const error = `Failed to update extension: ${extensionId} cannot be updated. (no git repository found at ${target})` | |
ctx.integration.error(error) | |
ctx.app.sendErrorToSession(sessionId, error) | |
return { error } | |
} | |
} else { | |
// Handle invalid/active extension case | |
const error = `Failed to update extension:\n${extensionId} appears to not be a valid/active extension.` | |
ctx.app.sendErrorToSession(sessionId, error) | |
return { error } | |
} | |
} | |
const script = { | |
name: 'extensions', | |
exec: async function (ctx, payload) { | |
const sessionId = ctx.session.sessionId | |
let [command, arg1] = [...payload] | |
ctx.integration.debug('extensions', command, arg1) | |
if (arg1?.length > 1) { | |
if (command === 'manifest') { | |
const response = await fetch(arg1) | |
if (!response.ok) { | |
throw new Error(`HTTP error! status: ${response.status}`) | |
} | |
const text = await response.text() | |
const manifest = yaml.safeLoad(text) | |
await ctx.app.sendMessageToSession(sessionId, 'Extension Manifest', 'text/plain', { | |
objects: [manifest], | |
commands: [ | |
{ | |
title: 'Install', | |
id: 'extensions', | |
args: ['add', manifest.origin] | |
} | |
] | |
}) | |
} else if (command === 'add') { | |
try { | |
// if it's not a git repository we need to resolve it. | |
if (!arg1.endsWith('.git')) { | |
await ctx.app.sendMessageToSession(sessionId, 'Loading extension manifest from: ' + arg1, 'text/plain') | |
const response = await fetch(arg1) | |
if (!response.ok) { | |
ctx.app.sendErrorToSession(sessionId, 'Failed to retrieve manifest' + response.status) | |
throw new Error(`HTTP error! status: ${response.status}`) | |
} | |
const text = await response.text() | |
const manifest = yaml.load(text) | |
if (manifest.origin !== undefined) { | |
arg1 = manifest.origin | |
} else { | |
ctx.app.sendErrorToSession(sessionId, 'Failed to install extension: No origin found in extension.yaml') | |
throw new Error('No origin found in extension.yaml') | |
} | |
await ctx.app.sendMessageToSession(sessionId, 'Downloaded extension manifest from: ' + arg1, 'text/plain', { objects: [manifest] }) | |
} | |
await ctx.app.sendMessageToSession(sessionId, 'Attempting to install extension from: ' + arg1 + '\nPlease wait...', 'text/plain') | |
const git = simpleGit({ baseDir: path.join(process.cwd(),'extensions') }) | |
await git.clone(arg1) | |
await ctx.app.sendMessageToSession(sessionId, 'Extension added, please restart the server.\n', 'text/plain',{ | |
commands: [ | |
{ | |
title: 'Restart Server', | |
id: 'rs', | |
args: [] | |
} | |
]} | |
); | |
await ctx.app.kvStorage?.set('extensions.dirty', true) | |
return true; | |
} catch (e) { | |
const error = 'Failed to install extension:\n' + e.message | |
console.log(e.message) | |
return { error } | |
} | |
} else if (command === 'show') { | |
const extensionId = arg1.replace(/[^a-zA-Z0-9-_]/g, '') | |
if (ctx.app.extensions.has(extensionId)) { | |
const extension = ctx.app.extensions.get(extensionId) | |
await ctx.app.sendMessageToSession(sessionId, JSON.stringify({ id: extension.id, title: extension.config.title, description: extension.config.description || 'No description.' }, null, 2), | |
'text/plain', | |
{ | |
commands: | |
[ | |
{ | |
title: 'Update', | |
id: 'extensions', | |
args: ['update', extensionId] | |
} | |
] | |
} | |
) | |
} | |
return true | |
} else if (command === 'update') { | |
const extensionId = arg1.replace(/[^a-zA-Z0-9-_]/g, '') | |
return await updateExtension(ctx, extensionId) | |
} else if (command === 'create') { | |
const extensionId = arg1.replace(/[^a-zA-Z0-9-_]/g, '') | |
const targetPath = path.join(process.cwd(), 'extensions', extensionId) | |
if (!(await validateDirectoryExists(targetPath))) { | |
await ensureDir(targetPath) | |
const git = simpleGit({ baseDir: targetPath }) | |
await git.clone(TEMPLATE_EXTENSION_REPO, targetPath) | |
// Remove git directory to re-init repository to a new one | |
rmSync(path.join(targetPath, '.git'), { recursive: true, force: true }) | |
const result = await git.init() | |
await ctx.app.sendMessageToSession(sessionId, `Extension ${extensionId} created at ${targetPath}, please restart server.\n`, 'text/plain', { objects: [result], | |
commands: [ | |
{ | |
title: 'Restart Server', | |
id: 'rs', | |
args: [] | |
} | |
] | |
}) | |
await ctx.app.kvStorage?.set('extensions.dirty', true) | |
return true | |
} else { | |
ctx.app.sendErrorToSession(sessionId, `An extension with the id ${extensionId} already exists.`) | |
} | |
} else if (command === 'remove') { | |
const extensionId = arg1.replace(/[^a-zA-Z0-9-_]/g, '') | |
await ctx.app.sendMessageToSession(sessionId, `To remove this extension, please delete the folder ${extensionId} in the server's extension folder and restart the server.`, 'text/plain', | |
{ | |
commands: [ | |
{ | |
title: 'Restart Server', | |
id: 'rs', | |
args: [] | |
} | |
] | |
} | |
) | |
} | |
} else { | |
if (command === 'installed') { | |
const allExtensions = ctx.app.extensions.all() | |
const commands = allExtensions.map(e => ({ id: 'extensions', title: 'Show ' + e.id, args: ['show', e.id] })) | |
await ctx.app.sendMessageToSession(sessionId, 'Installed Extensions\n\n' + allExtensions.map(e => `- ${e.id} ${e.config.title}: ${e.config.description || 'No description available'}`).join('\n'), | |
'text/plain', | |
{ | |
commands | |
}) | |
return true | |
} else if (command === 'list') { | |
const knownFile = await getKnownExtensions() | |
const commands = knownFile.core_extensions.concat(knownFile.known_extensions).map(function (e) { | |
if (ctx.app.extensions.has(e.id)) { | |
return { | |
id: 'extensions', title: '☑️ ' + e.title, args: ['show', e.id], classes: ['w-32'] | |
} | |
} else { | |
return { | |
id: 'extensions', title: '📥 ' + e.title, args: ['add', e.url], classes: ['w-32'] | |
} | |
} | |
} | |
).sort((a, b) => a.title.localeCompare(b.title)) | |
const extensionList = { | |
core: knownFile.core_extensions?.map(e => { | |
return {type: 'core', installed: `${ctx.app.extensions.has(e.id)}`, id: `${e.id}`, title: `${e.title}`, description: `${e.description}`, url: `${e.url}`}; | |
}).sort((a, b) => a.title.localeCompare(b.title)), | |
premium: knownFile.premium_extensions?.map(e => { | |
return {type: 'premium', installed: `${ctx.app.extensions.has(e.id)}`, id: `${e.id}`, title: `${e.title}`, description: `${e.description}`, url: `${e.url}`}; | |
}).sort((a, b) => a.title.localeCompare(b.title)), | |
known: knownFile.known_extensions?.map(e => { | |
return {type: 'known', installed: `${ctx.app.extensions.has(e.id)}`, id: `${e.id}`, title: `${e.title}`, description: `${e.description}`, url: `${e.url}`}; | |
}).sort((a, b) => a.title.localeCompare(b.title)), | |
available: knownFile.known_extensions?.filter(e => !e.deprecated).filter(e=>!ctx.app.extensions.has(e.id)).map(e => { | |
return {type: 'available', installed: `${ctx.app.extensions.has(e.id)}`, id: `${e.id}`, title: `${e.title}`, description: `${e.description}`, url: `${e.url}`}; | |
}).sort((a, b) => a.title.localeCompare(b.title)), | |
}; | |
await ctx.app.sendMessageToSession(sessionId, extensionList, 'omni/extension-list', { commands }) | |
return true | |
} else if (command === 'updateAll') { | |
const allExtensions = ctx.app.extensions.all() | |
for (const extension of allExtensions) { | |
try { | |
await updateExtension(ctx, extension.id) | |
} catch (ex) { | |
ctx.integration.error('Failed to update extension: ' + extension.id, ex) | |
ctx.app.sendErrorToSession(sessionId, 'Failed to update extension: ' + extension.id + ':' + ex.message) | |
} | |
} | |
return true | |
} | |
await ctx.app.sendMessageToSession(sessionId, `--- Extension Management --- | |
Use to install, manage and remove omnitool extensions.\n\ | |
Usage:\n/extensions add <git url> - add an extension | |
/extensions show <id> - show details on an extension | |
/extensions update <id> - update an extension | |
/extensions remove <name> - remove an extension | |
/extensions create <id> - create a new extension. | |
/extensions installed - show installed extensions | |
/extensions updateAll - update all installed extensions | |
/extensions list - list all known extensions | |
⚠️ WARNING ⚠️ | |
Extensions are executable code and have the same level access as omnitool itself. | |
Only install extensions from trusted sources! | |
To safeguard credentials from potentially malicious extensions, especially in a multi-user environment, consider performing an advanced install that sandboxes the key management and REST execution engine a separate machine.` | |
, | |
'text/plain', | |
{ | |
commands: | |
[ | |
{ | |
id: 'extensions', | |
title: 'List Available', | |
args: ['list'], | |
classes: ['w-32'] | |
}, | |
{ | |
id: 'extensions', | |
title: 'List Installed', | |
args: ['installed'], | |
classes: ['w-32'] | |
}, | |
{ | |
id: 'extensions', | |
title: 'Update All', | |
args: ['updateAll'], | |
classes: ['w-32'] | |
} | |
] | |
}) | |
return true | |
} | |
return true | |
} | |
} | |
export default script | |