gadget/gadget-code/src/web-cli.ts
2026-05-01 14:31:00 -04:00

572 lines
15 KiB
TypeScript

// src/web-cli.ts
// Copyright (C) 2026 Robert Colbert <rob.colbert@openplatform.us>
// All Rights Reserved
import { v4 as uuidv4 } from "uuid";
import "./lib/db.js";
import ApiClient, { ApiClientStatus } from "./models/api-client.js";
import User from "./models/user.js";
import AiProvider from "./models/ai-provider.js";
import ApiClientService from "./services/api-client.js";
import CryptoService from "./services/crypto.js";
import UserService from "./services/user.js";
import { DtpProcess } from "./lib/process.js";
import { createAiApi, type IAiLogger } from "@gadget/ai";
import {
type IAiModel,
type IAiModelCapabilities,
type IAiModelSettings,
} from "@gadget/api";
class DtpWebCli extends DtpProcess {
get name(): string {
return "DtpWebCli";
}
get slug(): string {
return "dtp-web-cli";
}
constructor() {
super();
}
async run(argv: string[]): Promise<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
const name = argv.shift();
const description = argv.shift();
const client = await ApiClientService.create({
name,
description,
});
this.log.info("api client added", {
client: {
_id: client._id,
secret: client.secret,
name: client.name,
},
});
}
async onApiClientList(_argv: string[]): Promise<void> {
const clients = await ApiClient.find({ status: ApiClientStatus.Active })
.sort({ name: 1 })
.lean();
console.log("Name".padEnd(20), "Client ID".padEnd(24), "Secret");
console.log(
"--------------------------------------------------------------------------------",
);
for (const client of clients) {
console.log(client.name.padEnd(20), client._id.toString(), client.secret);
}
}
async onApiClientSetStatus(argv: string[]): Promise<void> {
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<void> {
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<void> {
const action = argv.shift();
if (!action) {
throw new Error("must specify user command action");
}
switch (action) {
case "add":
return this.onUserAdd(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<void> {
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 onUserRemove(argv: string[]): Promise<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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 start(): Promise<void> {
await this.startServices();
}
async startServices(): Promise<void> {
await ApiClientService.start();
await (await import("./services/chat-session.js")).default.start();
}
}
(async () => {
try {
const cli = new DtpWebCli();
await cli.start();
await cli.run(process.argv.slice(2));
process.exit(0);
} catch (error) {
console.error((error as Error).message);
process.exit(-1);
}
})();