mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-28 14:15:24 +00:00
270 lines
8.3 KiB
TypeScript
270 lines
8.3 KiB
TypeScript
import {
|
|
createCurrentNotificationChannelRepository,
|
|
createCurrentAutomationRepository,
|
|
createCurrentCommandHistoryRepository,
|
|
createCurrentFleetRepository,
|
|
createCurrentHomepageItemRepository,
|
|
createCurrentHostRepository,
|
|
} from "../../../../../src/backend/database/repositories/factory.js";
|
|
import { num, objectSchema, type AiTool } from "./types.js";
|
|
import {
|
|
listSavedWorkspaces,
|
|
getNetworkTopology,
|
|
listSnippets,
|
|
} from "../services.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. Unavailable if the snippets plugin is disabled.",
|
|
category: "read",
|
|
parameters: objectSchema({}),
|
|
handler: async (_args, context) => {
|
|
const snippets = await listSnippets(context.userId);
|
|
if (!snippets) return { unavailable: true, snippets: [] };
|
|
return {
|
|
snippets: snippets.map((snippet) => ({
|
|
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 listSavedWorkspaces(context.userId);
|
|
if (workspaces === null) return { workspaces: [], unavailable: true };
|
|
return { workspaces };
|
|
},
|
|
},
|
|
{
|
|
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 getNetworkTopology(context.userId);
|
|
if (topology === null) return { topology: null, unavailable: true };
|
|
return { topology };
|
|
},
|
|
},
|
|
];
|