mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-30 07:05:24 +00:00
feat: make each plugin an npm workspace built by a termix-plugin CLI
This commit is contained in:
1 parent
4758016f41
commit
5796186a1c
448 files changed
+3087
-2003
No files matched your search
@@ -0,0 +1,278 @@
|
||||
import {
|
||||
createCurrentNotificationChannelRepository,
|
||||
createCurrentAutomationRepository,
|
||||
createCurrentCommandHistoryRepository,
|
||||
createCurrentFleetRepository,
|
||||
createCurrentHomepageItemRepository,
|
||||
createCurrentHostRepository,
|
||||
createCurrentNetworkTopologyRepository,
|
||||
createCurrentSnippetRepository,
|
||||
createCurrentWorkspaceRepository,
|
||||
} from "../../../../../src/backend/database/repositories/factory.js";
|
||||
import { num, objectSchema, type AiTool } from "./types.js";
|
||||
|
||||
/**
|
||||
* Read tools project explicit fields rather than spreading rows. Redaction runs
|
||||
* afterwards as a second line of defense, but the projection here is the
|
||||
* primary control: a field that is never selected cannot leak.
|
||||
*/
|
||||
|
||||
interface HostSummary {
|
||||
id: number;
|
||||
name: string | null;
|
||||
ip: string | null;
|
||||
port: number | null;
|
||||
username: string | null;
|
||||
folder: string | null;
|
||||
tags: unknown;
|
||||
protocol: string | null;
|
||||
enableTerminal: boolean | null;
|
||||
enableFileManager: boolean | null;
|
||||
enableTunnel: boolean | null;
|
||||
enableDocker: boolean | null;
|
||||
}
|
||||
|
||||
function toHostSummary(host: Record<string, any>): HostSummary {
|
||||
return {
|
||||
id: host.id,
|
||||
name: host.name ?? null,
|
||||
ip: host.ip ?? null,
|
||||
port: host.port ?? null,
|
||||
username: host.username ?? null,
|
||||
folder: host.folder ?? null,
|
||||
tags: host.tags ?? null,
|
||||
protocol: host.protocol ?? null,
|
||||
enableTerminal: host.enableTerminal ?? null,
|
||||
enableFileManager: host.enableFileManager ?? null,
|
||||
enableTunnel: host.enableTunnel ?? null,
|
||||
enableDocker: host.enableDocker ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
export const readTools: AiTool[] = [
|
||||
{
|
||||
name: "list_hosts",
|
||||
description:
|
||||
"List the user's SSH hosts with their names, addresses, folders and tags. Never returns passwords or keys. Call this before proposing anything that references a host.",
|
||||
category: "read",
|
||||
parameters: objectSchema({}),
|
||||
handler: async (_args, context) => {
|
||||
const hosts = await createCurrentHostRepository().listByUserId(
|
||||
context.userId,
|
||||
);
|
||||
return { hosts: hosts.map((host) => toHostSummary(host as any)) };
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "get_host",
|
||||
description:
|
||||
"Get one host's non-secret configuration by id. Use it to inspect settings before proposing an update.",
|
||||
category: "read",
|
||||
parameters: objectSchema(
|
||||
{ hostId: num("The host id, as returned by list_hosts") },
|
||||
["hostId"],
|
||||
),
|
||||
handler: async (args, context) => {
|
||||
const hostId = Number(args.hostId);
|
||||
const host = await createCurrentHostRepository().findByIdForUser(
|
||||
context.userId,
|
||||
hostId,
|
||||
);
|
||||
if (!host) return { error: "Host not found" };
|
||||
return { host: toHostSummary(host as any) };
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "list_fleets",
|
||||
description:
|
||||
"List the user's fleets. Fleets group hosts for bulk operations and inventory.",
|
||||
category: "read",
|
||||
parameters: objectSchema({}),
|
||||
handler: async (_args, context) => {
|
||||
const fleets = await createCurrentFleetRepository().listByUser(
|
||||
context.userId,
|
||||
);
|
||||
return {
|
||||
fleets: fleets.map((fleet: any) => ({
|
||||
id: fleet.id,
|
||||
name: fleet.name,
|
||||
description: fleet.description ?? null,
|
||||
})),
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "list_snippets",
|
||||
description:
|
||||
"List the user's saved command snippets, including their folder and the command text.",
|
||||
category: "read",
|
||||
parameters: objectSchema({}),
|
||||
handler: async (_args, context) => {
|
||||
const snippets = await createCurrentSnippetRepository().listOwnedSnippets(
|
||||
context.userId,
|
||||
);
|
||||
return {
|
||||
snippets: snippets.map((snippet: any) => ({
|
||||
id: snippet.id,
|
||||
name: snippet.name,
|
||||
content: snippet.content,
|
||||
folder: snippet.folder ?? null,
|
||||
description: snippet.description ?? null,
|
||||
})),
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "list_automations",
|
||||
description:
|
||||
"List the user's automations with their trigger kind and enabled state. Read this before proposing a change to an existing automation.",
|
||||
category: "read",
|
||||
parameters: objectSchema({}),
|
||||
handler: async (_args, context) => {
|
||||
const automations = await createCurrentAutomationRepository().list(
|
||||
context.userId,
|
||||
);
|
||||
return {
|
||||
automations: automations.map((automation: any) => ({
|
||||
id: automation.id,
|
||||
name: automation.name,
|
||||
description: automation.description ?? null,
|
||||
enabled: automation.enabled,
|
||||
lastRunAt: automation.lastRunAt ?? null,
|
||||
lastRunStatus: automation.lastRunStatus ?? null,
|
||||
})),
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "get_automation",
|
||||
description:
|
||||
"Get one automation's full definition (trigger and steps) by id.",
|
||||
category: "read",
|
||||
parameters: objectSchema(
|
||||
{
|
||||
automationId: num("The automation id, as returned by list_automations"),
|
||||
},
|
||||
["automationId"],
|
||||
),
|
||||
handler: async (args, context) => {
|
||||
const automation = await createCurrentAutomationRepository().findForUser(
|
||||
Number(args.automationId),
|
||||
context.userId,
|
||||
);
|
||||
if (!automation) return { error: "Automation not found" };
|
||||
return {
|
||||
automation: {
|
||||
id: (automation as any).id,
|
||||
name: (automation as any).name,
|
||||
enabled: (automation as any).enabled,
|
||||
definition: (automation as any).definition,
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "list_workspaces",
|
||||
description:
|
||||
"List the user's saved workspace layouts (named sets of open tabs and splits).",
|
||||
category: "read",
|
||||
parameters: objectSchema({}),
|
||||
handler: async (_args, context) => {
|
||||
const workspaces = await createCurrentWorkspaceRepository().listByUser(
|
||||
context.userId,
|
||||
);
|
||||
return {
|
||||
workspaces: workspaces.map((workspace: any) => ({
|
||||
id: workspace.id,
|
||||
name: workspace.name,
|
||||
isDefault: workspace.isDefault ?? false,
|
||||
})),
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "list_notification_channels",
|
||||
description:
|
||||
"List the user's notification channels by id, name and type. Channel configuration is never returned because it holds tokens.",
|
||||
category: "read",
|
||||
parameters: objectSchema({}),
|
||||
handler: async (_args, context) => {
|
||||
const channels =
|
||||
await createCurrentNotificationChannelRepository().listNotificationChannels(
|
||||
context.userId,
|
||||
);
|
||||
return {
|
||||
channels: channels.map((channel) => ({
|
||||
id: channel.id,
|
||||
name: channel.name,
|
||||
type: channel.type,
|
||||
enabled: channel.enabled === 1,
|
||||
})),
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "list_homepage_items",
|
||||
description: "List the user's homepage service-link tiles.",
|
||||
category: "read",
|
||||
parameters: objectSchema({}),
|
||||
handler: async (_args, context) => {
|
||||
const items = await createCurrentHomepageItemRepository().listByUserId(
|
||||
context.userId,
|
||||
);
|
||||
return {
|
||||
items: (items as any[]).map((item) => ({
|
||||
id: item.id,
|
||||
typeId: item.typeId,
|
||||
title: item.title ?? null,
|
||||
})),
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "get_command_history",
|
||||
description:
|
||||
"Recent commands the user has run on one host, newest first. Useful for understanding what they have been working on.",
|
||||
category: "read",
|
||||
parameters: objectSchema(
|
||||
{
|
||||
hostId: num("The host id, as returned by list_hosts"),
|
||||
limit: num("How many entries to return (default 25, max 100)"),
|
||||
},
|
||||
["hostId"],
|
||||
),
|
||||
handler: async (args, context) => {
|
||||
const limit = Math.min(Math.max(Number(args.limit) || 25, 1), 100);
|
||||
const hostId = Number(args.hostId);
|
||||
|
||||
// Ownership is enforced here rather than trusted from the model.
|
||||
const host = await createCurrentHostRepository().findByIdForUser(
|
||||
context.userId,
|
||||
hostId,
|
||||
);
|
||||
if (!host) return { error: "Host not found" };
|
||||
|
||||
const commands =
|
||||
await createCurrentCommandHistoryRepository().listCommandsForHost(
|
||||
context.userId,
|
||||
hostId,
|
||||
limit,
|
||||
);
|
||||
return { commands };
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "get_network_topology",
|
||||
description: "The user's saved network topology graph, if they have one.",
|
||||
category: "read",
|
||||
parameters: objectSchema({}),
|
||||
handler: async (_args, context) => {
|
||||
const topology =
|
||||
await createCurrentNetworkTopologyRepository().findByUserId(
|
||||
context.userId,
|
||||
);
|
||||
if (!topology) return { topology: null };
|
||||
return { topology: (topology as any).data ?? null };
|
||||
},
|
||||
},
|
||||
];
|
||||
Reference in new issue
Block a user