// src/web-cli.ts // Copyright (C) 2026 Robert Colbert // All Rights Reserved import { v4 as uuidv4 } from "uuid"; import "./lib/db.js"; /* * Models */ import ApiClient, { ApiClientStatus, IApiClient } from "./models/api-client.js"; import User from "./models/user.js"; import AiProvider from "./models/ai-provider.js"; /* * Services */ import { startServices, stopServices, ApiClientService, UserService, CryptoService, } from "./services/index.js"; /* * App Logic */ import { createAiApi, type IAiLogger } from "@gadget/ai"; import { IUser, type IAiModel, type IAiModelCapabilities, type IAiModelSettings, } from "@gadget/api"; import { DtpProcess } from "./lib/process.js"; class DtpWebCli extends DtpProcess { get name(): string { return "DtpWebCli"; } get slug(): string { return "dtp-web-cli"; } constructor() { super(); } async run(argv: string[]): Promise { const cmd = argv.shift(); if (!cmd) { throw new Error("must specify command"); } switch (cmd) { case "admin": return this.onAdminCmd(argv); case "api-client": return this.onApiClientCmd(argv); case "user": return this.onUserCmd(argv); case "provider": return this.onProviderCmd(argv); default: break; } throw new Error(`unknown command: ${cmd}`); } async onAdminCmd(argv: string[]): Promise { const action = argv.shift(); if (!action) { throw new Error("must specify user command action"); } switch (action) { case "grant": return this.onAdminGrant(argv); case "revoke": return this.onAdminRevoke(argv); default: break; } throw new Error(`unknown user command action: ${action}`); } async onAdminGrant(argv: string[]): Promise { const email = argv.shift(); if (!email) { throw new Error("must specify email for admin grant"); } const user = await User.findOne({ email_lc: email.trim().toLowerCase(), }); if (!user) { throw new Error(`user ${email} not found`); } user.flags.isAdmin = true; await user.save(); this.log.info(`admin rights granted to ${email}`); } async onAdminRevoke(argv: string[]): Promise { const email = argv.shift(); if (!email) { throw new Error("must specify email for admin revoke"); } const user = await User.findOne({ email_lc: email.trim().toLowerCase(), }); if (!user) { throw new Error(`user ${email} not found`); } user.flags.isAdmin = false; await user.save(); this.log.info(`admin rights revoked from ${email}`); } async onApiClientCmd(argv: string[]): Promise { const action = argv.shift(); if (!action) { throw new Error("must specify api client command action"); } switch (action) { case "add": return this.onApiClientAdd(argv); case "ls": return this.onApiClientList(argv); case "status": return this.onApiClientSetStatus(argv); case "remove": return this.onApiClientRemove(argv); default: break; } throw new Error(`unknown api client command action: ${action}`); } async onApiClientAdd(argv: string[]): Promise { const name = argv.shift(); const description = argv.shift(); const email = argv.shift(); let user; if (email) { user = await UserService.getByEmail(email); if (!user) { throw new Error("user not found"); } } const definition: Partial = { name, description, }; if (user) { definition.user = user._id; } const client = await ApiClientService.create(definition); this.printApiClientList([client]); } async onApiClientList(_argv: string[]): Promise { const clients: IApiClient[] = await ApiClient.find({ status: ApiClientStatus.Active, }) .sort({ name: 1 }) .populate([{ path: "user", select: "-passwordSalt -password" }]) .lean(); this.printApiClientList(clients); } printApiClientList(clients: IApiClient[]) { console.log("Name".padEnd(20), "Client ID".padEnd(24), "Secret"); console.log( "--------------------------------------------------------------------------------", ); for (const client of clients) { let log = `${client.name.padEnd(20)} ${client._id.toString()} ${client.secret}`; if (client.user) { const user = client.user as IUser; log += ` ${user._id} ${user.email}`; } console.log(log); } } async onApiClientSetStatus(argv: string[]): Promise { const clientId = argv.shift(); if (!clientId) { throw new Error("client ID is required"); } const client = await ApiClientService.getById(clientId); if (!client) { throw new Error("Client not found"); } const status = argv.shift() as ApiClientStatus; if (!status) { throw new Error("New client status is required"); } await ApiClientService.setStatus(client, status); this.log.info("API client status updated", { _id: clientId, status }); } async onApiClientRemove(argv: string[]): Promise { const clientId = argv.shift(); if (!clientId) { throw new Error("client ID is required"); } const client = await ApiClientService.getById(clientId); if (!client) { throw new Error("Client not found"); } await ApiClientService.remove(client); this.log.info("API client removed", { _id: clientId }); } async onUserCmd(argv: string[]): Promise { const action = argv.shift(); if (!action) { throw new Error("must specify user command action"); } switch (action) { case "add": return this.onUserAdd(argv); case "view": return this.onUserView(argv); case "password": return this.onUserPassword(argv); case "remove": return this.onUserRemove(argv); case "ban": return this.onUserBan(argv); default: break; } throw new Error(`unknown user command action: ${action}`); } async onUserAdd(argv: string[]): Promise { const email = argv.shift(); if (!email) { throw new Error("must specify email address"); } const password = argv.shift(); if (!password) { throw new Error("must specify password"); } const displayName: string | undefined = argv.shift(); const user = await UserService.create(email, password, displayName); this.log.info(`user created: id:${user._id}, email:${user.email}`); } async onUserView(argv: string[]): Promise { const email = argv.shift(); if (!email) { throw new Error("must specify email address"); } const user = await UserService.getByEmail(email); if (!user) { throw new Error("user not found"); } this.log.info("user account", user); } async onUserRemove(argv: string[]): Promise { let email = argv.shift(); if (!email) { throw new Error("must specify email address"); } email = email.trim().toLowerCase(); const user = await User.findOne({ email }).lean(); if (!user) { throw new Error(`user not found: ${email}`); } await User.deleteOne({ _id: user._id }); this.log.info(`user ${email} removed`); } async onUserBan(argv: string[]): Promise { const email = argv.shift(); if (!email) { throw new Error("must specify email address"); } const email_lc = email.trim().toLowerCase(); const user = await User.findOne({ email_lc }).lean(); if (!user) { throw new Error(`user not found: ${email}`); } await User.updateOne( { _id: user._id }, { $set: { "flags.isBanned": true, }, }, ); this.log.info(`user ${email} banned`); } async onUserUnban(argv: string[]): Promise { const email = argv.shift(); if (!email) { throw new Error("must specify email address"); } const email_lc = email.trim().toLowerCase(); const user = await User.findOne({ email_lc }).lean(); if (!user) { throw new Error(`user not found: ${email}`); } await User.updateOne( { _id: user._id }, { $set: { "flags.isBanned": false, }, }, ); this.log.info(`user ${email} unbanned`); } async onUserPassword(argv: string[]): Promise { let email = argv.shift(); if (!email) { throw new Error("must specify email address"); } const password = argv.shift(); if (!password) { throw new Error("must specify password"); } const email_lc = email.trim().toLowerCase(); const user = await User.findOne({ email_lc }); if (!user) { throw new Error(`user not found: ${email}`); } user.passwordSalt = uuidv4(); user.password = await CryptoService.maskPassword( user.passwordSalt, password, ); await user.save(); this.log.info(`user ${email} password changed`); } async onProviderCmd(argv: string[]): Promise { const action = argv.shift(); if (!action) { throw new Error("must specify provider command action"); } switch (action) { case "add": return this.onProviderAdd(argv); case "ls": return this.onProviderList(argv); case "status": return this.onProviderSetStatus(argv); case "remove": return this.onProviderRemove(argv); case "probe": return this.onProviderProbe(argv); default: break; } throw new Error(`unknown provider command action: ${action}`); } async onProviderAdd(argv: string[]): Promise { const name = argv.shift(); if (!name) { throw new Error("must specify provider name"); } const sdkType = argv.shift(); if (!sdkType) { throw new Error("must specify SDK type (ollama or openai)"); } if (sdkType !== "ollama" && sdkType !== "openai") { throw new Error("SDK type must be 'ollama' or 'openai'"); } const baseUrl = argv.shift(); if (!baseUrl) { throw new Error("must specify base URL"); } const apiKey = argv.shift(); if (sdkType === "openai" && !apiKey) { throw new Error("API key required for OpenAI providers"); } // Check if provider with this name already exists const existing = await AiProvider.findOne({ baseUrl }); if (existing) { throw new Error(`provider with name '${baseUrl}' already exists`); } const provider = new AiProvider({ name, apiType: sdkType, baseUrl, apiKey: apiKey || "", enabled: true, models: [], lastModelRefresh: new Date(), }); await provider.save(); this.log.info("provider added", { _id: provider._id, name: provider.name, apiType: provider.apiType, baseUrl: provider.baseUrl, }); // Auto-probe for models this.log.info("probing provider for models..."); await this.onProviderProbe([provider._id]); } async onProviderList(_argv: string[]): Promise { const providers = await AiProvider.find({}).sort({ name: 1 }).lean(); console.log( "Name".padEnd(20), "ID".padEnd(24), "Type".padEnd(8), "URL".padEnd(30), "Models", "Enabled", ); console.log( "------------------------------------------------------------------------------------------------------------", ); for (const provider of providers) { console.log( provider.name.padEnd(20), provider._id.toString().padEnd(24), provider.apiType.padEnd(8), provider.baseUrl.padEnd(30), String(provider.models.length).padEnd(6), provider.enabled ? "Yes" : "No", ); } } async onProviderSetStatus(argv: string[]): Promise { const providerId = argv.shift(); if (!providerId) { throw new Error("provider ID is required"); } const provider = await AiProvider.findById(providerId); if (!provider) { throw new Error("Provider not found"); } const status = argv.shift(); if (!status) { throw new Error("New provider status is required (active or inactive)"); } if (status !== "active" && status !== "inactive") { throw new Error("Status must be 'active' or 'inactive'"); } provider.enabled = status === "active"; await provider.save(); this.log.info("Provider status updated", { _id: providerId, enabled: provider.enabled, }); } async onProviderRemove(argv: string[]): Promise { const providerId = argv.shift(); if (!providerId) { throw new Error("provider ID is required"); } const provider = await AiProvider.findById(providerId).select("+apiKey"); if (!provider) { throw new Error("Provider not found"); } this.log.info("Provider removed", { _id: providerId, name: provider.name, }); } async onProviderProbe(argv: string[]): Promise { const providerId = argv.shift(); if (!providerId) { throw new Error("provider ID is required"); } const provider = await AiProvider.findById(providerId).select("+apiKey"); if (!provider) { throw new Error("Provider not found"); } this.log.info("probing provider for models", { name: provider.name, apiType: provider.apiType, }); const cliLogger: IAiLogger = { debug: async (msg, meta) => this.log.debug(msg, meta), info: async (msg, meta) => this.log.info(msg, meta), warn: async (msg, meta) => this.log.warn(msg, meta), error: async (msg, meta) => this.log.error(msg, meta), }; const api = createAiApi( { _id: provider._id, name: provider.name, sdk: provider.apiType, baseUrl: provider.baseUrl, apiKey: provider.apiKey, }, cliLogger, ); this.log.info("fetching model list from provider..."); const listResult = await api.listModels(); this.log.info(`found ${listResult.models.length} models`, { count: listResult.models.length, }); const models: IAiModel[] = []; for (const modelInfo of listResult.models) { this.log.info(`probing model: ${modelInfo.id}`); try { const probeResult = await api.probeModel(modelInfo.id); const model: IAiModel = { id: modelInfo.id, name: modelInfo.name, parameterCount: modelInfo.parameterCount, parameterLabel: modelInfo.parameterLabel, contextWindow: modelInfo.contextWindow, capabilities: probeResult.capabilities as IAiModelCapabilities, settings: probeResult.settings as IAiModelSettings | undefined, }; models.push(model); this.log.info(`model probed successfully`, { model: model.id, capabilities: model.capabilities, }); } catch (error) { this.log.error(`failed to probe model ${modelInfo.id}`, { error: (error as Error).message, }); } } provider.models = models; provider.lastModelRefresh = new Date(); await provider.save(); this.log.info(`model discovery complete`, { totalModels: models.length, models: models.map((m) => ({ id: m.id, name: m.name, capabilities: m.capabilities, })), }); } } (async () => { try { await startServices(); const cli = new DtpWebCli(); await cli.run(process.argv.slice(2)); await stopServices(); process.exit(0); } catch (error) { console.error((error as Error).message); process.exit(-1); } })();