mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-28 22:25:24 +00:00
feat: wire ai, automations, fleets and ssh-terminal to snippets (B2)
This commit is contained in:
1 parent
3656caf168
commit
c77301577f
16 files changed
+473
-52
No files matched your search
@@ -51,3 +51,119 @@ export async function getNetworkTopology(
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export interface SnippetSummary {
|
||||
id: number;
|
||||
name: string;
|
||||
content: string;
|
||||
description: string | null;
|
||||
isNote: boolean;
|
||||
folder: string | null;
|
||||
}
|
||||
|
||||
/** Another plugin's snippets, as ctx.services.get("snippets.access") returns them. */
|
||||
interface SnippetsAccess {
|
||||
list: () => Promise<SnippetSummary[]>;
|
||||
get: (id: number) => Promise<{
|
||||
id: number;
|
||||
name: string;
|
||||
content: string;
|
||||
isNote: boolean;
|
||||
} | null>;
|
||||
create: (input: {
|
||||
name: string;
|
||||
content: string;
|
||||
description?: string | null;
|
||||
folder?: string | null;
|
||||
}) => Promise<{ id: number; name: string }>;
|
||||
update: (
|
||||
id: number,
|
||||
changes: {
|
||||
name?: string;
|
||||
content?: string;
|
||||
description?: string | null;
|
||||
folder?: string | null;
|
||||
},
|
||||
) => Promise<void>;
|
||||
remove: (id: number) => Promise<boolean>;
|
||||
}
|
||||
|
||||
/**
|
||||
* The user's own snippets, or null when the snippets plugin is off or the
|
||||
* user may not use it. Optional: the list_snippets tool disappears without
|
||||
* it, and the propose_*_snippet tools fail with a clear message.
|
||||
*/
|
||||
export async function listSnippets(
|
||||
userId: string,
|
||||
): Promise<SnippetSummary[] | null> {
|
||||
if (!current) return null;
|
||||
try {
|
||||
return await current
|
||||
.get<SnippetsAccess>("snippets.access", { userId })
|
||||
.list();
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getSnippet(
|
||||
userId: string,
|
||||
id: number,
|
||||
): Promise<{
|
||||
id: number;
|
||||
name: string;
|
||||
content: string;
|
||||
isNote: boolean;
|
||||
} | null> {
|
||||
if (!current) return null;
|
||||
try {
|
||||
return await current
|
||||
.get<SnippetsAccess>("snippets.access", { userId })
|
||||
.get(id);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Requires the snippets plugin, unlike the read helpers above: a failed
|
||||
* proposal apply must surface as an error the user sees, not silently no-op.
|
||||
*/
|
||||
function requireSnippetsAccess(userId: string): SnippetsAccess {
|
||||
if (!current) {
|
||||
throw new Error("The snippets plugin is disabled");
|
||||
}
|
||||
return current.get<SnippetsAccess>("snippets.access", { userId });
|
||||
}
|
||||
|
||||
export async function createSnippet(
|
||||
userId: string,
|
||||
input: {
|
||||
name: string;
|
||||
content: string;
|
||||
description?: string | null;
|
||||
folder?: string | null;
|
||||
},
|
||||
): Promise<{ id: number; name: string }> {
|
||||
return requireSnippetsAccess(userId).create(input);
|
||||
}
|
||||
|
||||
export async function updateSnippet(
|
||||
userId: string,
|
||||
id: number,
|
||||
changes: {
|
||||
name?: string;
|
||||
content?: string;
|
||||
description?: string | null;
|
||||
folder?: string | null;
|
||||
},
|
||||
): Promise<void> {
|
||||
return requireSnippetsAccess(userId).update(id, changes);
|
||||
}
|
||||
|
||||
export async function deleteSnippet(
|
||||
userId: string,
|
||||
id: number,
|
||||
): Promise<boolean> {
|
||||
return requireSnippetsAccess(userId).remove(id);
|
||||
}
|
||||
@@ -2,13 +2,18 @@ import {
|
||||
createCurrentAutomationRepository,
|
||||
createCurrentFleetRepository,
|
||||
createCurrentHostRepository,
|
||||
createCurrentSnippetRepository,
|
||||
} from "../../../../../src/backend/database/repositories/factory.js";
|
||||
import { validateDefinition } from "../../../../automations/src/backend/routes.js";
|
||||
import { resolveHostById } from "../../../../../src/backend/hosts/host-resolver.js";
|
||||
import { execCommand } from "../../../../../src/backend/hosts/metrics-shared/common-utils.js";
|
||||
import { withSshConnection } from "../ssh.js";
|
||||
import { getTool } from "./catalog.js";
|
||||
import {
|
||||
createSnippet,
|
||||
updateSnippet,
|
||||
deleteSnippet,
|
||||
getSnippet,
|
||||
} from "../services.js";
|
||||
|
||||
/** Approved commands get a bounded window rather than hanging the request. */
|
||||
const COMMAND_TIMEOUT_MS = 60_000;
|
||||
@@ -125,18 +130,15 @@ export async function applyProposal(
|
||||
}
|
||||
|
||||
case "propose_create_snippet": {
|
||||
const created = await createCurrentSnippetRepository().createSnippet(
|
||||
userId,
|
||||
{
|
||||
name: requireString(payload.name, "name"),
|
||||
content: requireString(payload.content, "content"),
|
||||
description: optionalString(payload.description),
|
||||
folder: optionalString(payload.folder),
|
||||
} as any,
|
||||
);
|
||||
const created = await createSnippet(userId, {
|
||||
name: requireString(payload.name, "name"),
|
||||
content: requireString(payload.content, "content"),
|
||||
description: optionalString(payload.description),
|
||||
folder: optionalString(payload.folder),
|
||||
});
|
||||
return {
|
||||
ok: true,
|
||||
summary: `Created snippet ${(created as any)?.name ?? ""}`.trim(),
|
||||
summary: `Created snippet ${created.name}`.trim(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -144,10 +146,7 @@ export async function applyProposal(
|
||||
const snippetId = requireNumber(payload.snippetId, "snippetId");
|
||||
const changes = (payload.changes ?? {}) as Record<string, unknown>;
|
||||
|
||||
const existing = await createCurrentSnippetRepository().findOwnedById(
|
||||
userId,
|
||||
snippetId,
|
||||
);
|
||||
const existing = await getSnippet(userId, snippetId);
|
||||
if (!existing) throw new Error("Snippet not found");
|
||||
|
||||
const updates: Record<string, unknown> = {};
|
||||
@@ -164,20 +163,13 @@ export async function applyProposal(
|
||||
return { ok: false, summary: "Nothing to change" };
|
||||
}
|
||||
|
||||
await createCurrentSnippetRepository().updateSnippet(
|
||||
userId,
|
||||
snippetId,
|
||||
updates as any,
|
||||
);
|
||||
await updateSnippet(userId, snippetId, updates);
|
||||
return { ok: true, summary: `Updated snippet ${snippetId}` };
|
||||
}
|
||||
|
||||
case "propose_delete_snippet": {
|
||||
const snippetId = requireNumber(payload.snippetId, "snippetId");
|
||||
const deleted = await createCurrentSnippetRepository().deleteSnippet(
|
||||
userId,
|
||||
snippetId,
|
||||
);
|
||||
const deleted = await deleteSnippet(userId, snippetId);
|
||||
if (!deleted) throw new Error("Snippet not found");
|
||||
return { ok: true, summary: `Deleted snippet ${snippetId}` };
|
||||
}
|
||||
|
||||
@@ -5,10 +5,13 @@ import {
|
||||
createCurrentFleetRepository,
|
||||
createCurrentHomepageItemRepository,
|
||||
createCurrentHostRepository,
|
||||
createCurrentSnippetRepository,
|
||||
} from "../../../../../src/backend/database/repositories/factory.js";
|
||||
import { num, objectSchema, type AiTool } from "./types.js";
|
||||
import { listSavedWorkspaces, getNetworkTopology } from "../services.js";
|
||||
import {
|
||||
listSavedWorkspaces,
|
||||
getNetworkTopology,
|
||||
listSnippets,
|
||||
} from "../services.js";
|
||||
|
||||
/**
|
||||
* Read tools project explicit fields rather than spreading rows. Redaction runs
|
||||
@@ -103,15 +106,14 @@ export const readTools: AiTool[] = [
|
||||
{
|
||||
name: "list_snippets",
|
||||
description:
|
||||
"List the user's saved command snippets, including their folder and the command text.",
|
||||
"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 createCurrentSnippetRepository().listOwnedSnippets(
|
||||
context.userId,
|
||||
);
|
||||
const snippets = await listSnippets(context.userId);
|
||||
if (!snippets) return { unavailable: true, snippets: [] };
|
||||
return {
|
||||
snippets: snippets.map((snippet: any) => ({
|
||||
snippets: snippets.map((snippet) => ({
|
||||
id: snippet.id,
|
||||
name: snippet.name,
|
||||
content: snippet.content,
|
||||
|
||||
Reference in new issue
Block a user