diff --git a/packages/toolpack-sdk/README.md b/packages/toolpack-sdk/README.md index 8e1aadd..bd5ed98 100644 --- a/packages/toolpack-sdk/README.md +++ b/packages/toolpack-sdk/README.md @@ -1,6 +1,6 @@ # Toolpack SDK -A unified TypeScript/Node.js SDK for building AI-powered applications with multiple providers, 79 built-in tools, a workflow engine, and a flexible mode system — all through a single API. +A unified TypeScript/Node.js SDK for building AI-powered applications with multiple providers, 90 built-in tools, a workflow engine, and a flexible mode system — all through a single API. [![npm version](https://img.shields.io/npm/v/toolpack-sdk.svg)](https://www.npmjs.com/package/toolpack-sdk) [![License: Apache 2.0](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) @@ -18,7 +18,7 @@ A unified TypeScript/Node.js SDK for building AI-powered applications with multi - **Mode System** — Built-in Agent and Chat modes, plus `createMode()` for custom modes with tool filtering - **HITL Confirmation** — Human-in-the-loop approval for high-risk operations with configurable bypass rules - **Custom Providers** — Bring your own provider by implementing the `ProviderAdapter` interface -- **79 Built-in Tools** across 10 categories: +- **90 Built-in Tools** across 11 categories: - **MCP Tool Server Integration** — dynamically bridge external Model Context Protocol servers into Toolpack as first-class tools via `createMcpToolProject()` and `disconnectMcpToolProject()`. | Category | Tools | Description | @@ -33,6 +33,7 @@ A unified TypeScript/Node.js SDK for building AI-powered applications with multi | **`system-tools`** | 5 | System info — env vars, cwd, disk usage, system info, set env | | **`diff-tools`** | 3 | Patch operations — create, apply, and preview diffs | | **`cloud-tools`** | 3 | Deployments — deploy, status, list (via Netlify) | +| **`k8s-tools`** | 11 | Kubernetes cluster inspection and management via kubectl | | **`mcp-tools`** | 2 | MCP integration — createMcpToolProject, disconnectMcpToolProject | ## Quick Start @@ -59,7 +60,7 @@ const sdk = await Toolpack.init({ anthropic: {}, // Reads ANTHROPIC_API_KEY from env }, defaultProvider: 'openai', - tools: true, // Load all 79 built-in tools + tools: true, // Load all 90 built-in tools defaultMode: 'agent', // Agent mode with workflow engine }); @@ -91,6 +92,44 @@ const sdk = await Toolpack.init({ }); ``` +## Kubernetes Tools + +Toolpack SDK now includes a dedicated Kubernetes tool category that exposes `kubectl`-backed operations when `tools: true` is enabled. Use these tools to inspect cluster state, fetch pod logs, apply manifests, and wait for rollout status. + +```typescript +const sdk = await Toolpack.init({ + provider: 'openai', + tools: true, + defaultMode: 'agent', +}); + +const podsResponse = await sdk.generate({ + model: 'gpt-4o', + messages: [ + { + role: 'user', + content: 'List pods in the default namespace using Kubernetes tools.', + }, + ], +}); +console.log(podsResponse.content); + +const applyResponse = await sdk.generate({ + model: 'gpt-4o', + messages: [ + { + role: 'user', + content: 'Apply the manifest at ./deploy/my-app.yaml to the staging namespace using Kubernetes tools.', + }, + ], +}); +console.log(applyResponse.content); +``` + +> Requires `kubectl` installed and configured with a valid kubeconfig. + +See `packages/toolpack-sdk/docs/examples/kubernetes-usage.ts` for a complete example. + ## Providers ### Built-in Providers @@ -509,7 +548,7 @@ client.on('tool:failed', (event) => { /* ... */ }); ## Custom Tools -In addition to the 79 built-in tools, you can create and register your own custom tool projects using `createToolProject()`: +In addition to the 90 built-in tools, you can create and register your own custom tool projects using `createToolProject()`: ```typescript import { Toolpack, createToolProject } from 'toolpack-sdk'; @@ -972,7 +1011,7 @@ toolpack-sdk/ │ │ └── ollama/ # Ollama adapter + provider (auto-discovery) │ ├── modes/ # Mode system (Agent, Chat, createMode) │ ├── workflows/ # Workflow engine (planner, step executor, progress) -│ ├── tools/ # 79 built-in tools + registry + router + BM25 search +│ ├── tools/ # 90 built-in tools + registry + router + BM25 search │ │ ├── fs-tools/ # File system (18 tools) │ │ ├── coding-tools/ # Code analysis (12 tools) │ │ ├── git-tools/ # Git operations (9 tools) @@ -983,6 +1022,7 @@ toolpack-sdk/ │ │ ├── system-tools/ # System info (5 tools) │ │ ├── diff-tools/ # Patch operations (3 tools) │ │ ├── cloud-tools/ # Deployments (3 tools) +│ │ ├── k8s-tools/ # Kubernetes management (11 tools) │ │ ├── registry.ts # Tool registry and loading │ │ ├── router.ts # Tool routing and filtering │ │ └── search/ # BM25 tool discovery engine (internal) @@ -998,7 +1038,7 @@ toolpack-sdk/ **Current Version:** 0.1.0 - ✓ **4 Built-in Providers** — OpenAI, Anthropic, Gemini, Ollama (+ custom provider API) -- ✓ **79 Built-in Tools** — fs, exec, git, diff, web, coding, db, cloud, http, system +- ✓ **90 Built-in Tools** — fs, exec, git, diff, web, coding, db, cloud, http, system, Kubernetes - ✓ **Workflow Engine** — AI-driven planning, step execution, retries, dynamic steps, progress events - ✓ **Mode System** — Agent, Coding, Chat, and custom modes via `createMode()` with `blockAllTools` support - ✓ **Tool Search** — BM25-based on-demand tool discovery for large tool libraries diff --git a/packages/toolpack-sdk/docs/examples/kubernetes-usage.ts b/packages/toolpack-sdk/docs/examples/kubernetes-usage.ts new file mode 100644 index 0000000..c1b0649 --- /dev/null +++ b/packages/toolpack-sdk/docs/examples/kubernetes-usage.ts @@ -0,0 +1,61 @@ +import { Toolpack } from 'toolpack-sdk'; + +async function runKubernetesExample() { + const sdk = await Toolpack.init({ + provider: 'openai', + tools: true, + defaultMode: 'agent', + }); + + console.log('Listing pods in the default namespace...'); + const podsResponse = await sdk.generate({ + model: 'gpt-4o', + messages: [ + { + role: 'user', + content: 'Use the Kubernetes tools to list pods in the default namespace and return the results.', + }, + ], + }); + console.log(podsResponse.content); + + console.log('Applying a manifest to staging...'); + const applyResponse = await sdk.generate({ + model: 'gpt-4o', + messages: [ + { + role: 'user', + content: 'Apply the manifest at ./deploy/my-app.yaml to the staging namespace using Kubernetes tools.', + }, + ], + }); + console.log(applyResponse.content); + + console.log('Waiting for the deployment rollout...'); + const rolloutResponse = await sdk.generate({ + model: 'gpt-4o', + messages: [ + { + role: 'user', + content: 'Wait for the my-app deployment rollout to complete in the staging namespace.', + }, + ], + }); + console.log(rolloutResponse.content); + + console.log('Fetching logs from the running pod...'); + const logsResponse = await sdk.generate({ + model: 'gpt-4o', + messages: [ + { + role: 'user', + content: 'Fetch the last 200 lines of logs from the pod my-app-12345 in the staging namespace.', + }, + ], + }); + console.log(logsResponse.content); +} + +runKubernetesExample().catch((error) => { + console.error('Kubernetes example failed:', error); +}); diff --git a/packages/toolpack-sdk/package.json b/packages/toolpack-sdk/package.json index 6cf52a3..164f995 100644 --- a/packages/toolpack-sdk/package.json +++ b/packages/toolpack-sdk/package.json @@ -1,7 +1,7 @@ { "name": "toolpack-sdk", "version": "1.3.0", - "description": "Unified TypeScript SDK for AI providers (OpenAI, Anthropic, Gemini, Ollama) with 72 built-in tools, workflow engine, and mode system for building AI-powered applications", + "description": "Unified TypeScript SDK for AI providers (OpenAI, Anthropic, Gemini, Ollama) with 90 built-in tools, workflow engine, and mode system for building AI-powered applications", "engines": { "node": ">=20" }, diff --git a/packages/toolpack-sdk/src/tools/index.ts b/packages/toolpack-sdk/src/tools/index.ts index 12c68c3..6889679 100644 --- a/packages/toolpack-sdk/src/tools/index.ts +++ b/packages/toolpack-sdk/src/tools/index.ts @@ -74,5 +74,13 @@ export { cloudDeployTool, cloudStatusTool, cloudListTool, } from './cloud-tools/index.js'; +export { + k8sToolsProject, + k8sListPodsTool, k8sDescribeTool, k8sGetLogsTool, + k8sApplyManifestTool, k8sDeleteResourceTool, k8sListServicesTool, + k8sListDeploymentsTool, k8sGetConfigMapTool, + k8sSwitchContextTool, k8sGetNamespacesTool, k8sWaitForDeploymentTool, +} from './k8s-tools/index.js'; + export{ McpToolManager,createMcpToolProject,disconnectMcpToolProject } from './mcp-tools/index.js'; export type { McpToolsConfig, McpServerConfig } from './mcp-tools/index.js'; diff --git a/packages/toolpack-sdk/src/tools/k8s-tools/index.test.ts b/packages/toolpack-sdk/src/tools/k8s-tools/index.test.ts new file mode 100644 index 0000000..808516b --- /dev/null +++ b/packages/toolpack-sdk/src/tools/k8s-tools/index.test.ts @@ -0,0 +1,65 @@ +import { expect, test, describe } from 'vitest'; +import { k8sToolsProject } from './index.js'; +import { + k8sListPodsTool, + k8sDescribeTool, + k8sGetLogsTool, + k8sApplyManifestTool, + k8sDeleteResourceTool, + k8sListServicesTool, + k8sListDeploymentsTool, + k8sGetConfigMapTool, + k8sSwitchContextTool, + k8sGetNamespacesTool, + k8sWaitForDeploymentTool, +} from './tools.js'; + +describe('k8s-tools', () => { + const expectedToolNames = [ + 'k8s.list_pods', + 'k8s.describe', + 'k8s.get_logs', + 'k8s.apply_manifest', + 'k8s.delete_resource', + 'k8s.list_services', + 'k8s.list_deployments', + 'k8s.get_config_map', + 'k8s.switch_context', + 'k8s.get_namespaces', + 'k8s.wait_for_deployment', + ]; + + test('exports the expected Kubernetes tool names', () => { + expect(k8sToolsProject.manifest.tools).toEqual(expectedToolNames); + }); + + test('exports tool definitions with execute functions', () => { + const tools = [ + k8sListPodsTool, + k8sDescribeTool, + k8sGetLogsTool, + k8sApplyManifestTool, + k8sDeleteResourceTool, + k8sListServicesTool, + k8sListDeploymentsTool, + k8sGetConfigMapTool, + k8sSwitchContextTool, + k8sGetNamespacesTool, + k8sWaitForDeploymentTool, + ]; + + tools.forEach((tool) => { + expect(tool).toHaveProperty('execute'); + expect(typeof tool.execute).toBe('function'); + }); + }); + + test('k8s tools expose JSON output and dry-run schema options', () => { + expect(k8sListPodsTool.parameters.properties).toHaveProperty('output'); + expect(k8sListDeploymentsTool.parameters.properties).toHaveProperty('output'); + expect(k8sListServicesTool.parameters.properties).toHaveProperty('output'); + expect(k8sGetNamespacesTool.parameters.properties).toHaveProperty('output'); + expect(k8sApplyManifestTool.parameters.properties).toHaveProperty('dryRun'); + expect(k8sDeleteResourceTool.parameters.properties).toHaveProperty('dryRun'); + }); +}); diff --git a/packages/toolpack-sdk/src/tools/k8s-tools/index.ts b/packages/toolpack-sdk/src/tools/k8s-tools/index.ts new file mode 100644 index 0000000..dc0efd4 --- /dev/null +++ b/packages/toolpack-sdk/src/tools/k8s-tools/index.ts @@ -0,0 +1,66 @@ +import type { ToolProject } from '../types.js'; +import { + k8sApplyManifestTool, + k8sDeleteResourceTool, + k8sDescribeTool, + k8sGetConfigMapTool, + k8sGetLogsTool, + k8sGetNamespacesTool, + k8sListDeploymentsTool, + k8sListPodsTool, + k8sListServicesTool, + k8sSwitchContextTool, + k8sWaitForDeploymentTool, +} from './tools.js'; + +export { + k8sListPodsTool, + k8sDescribeTool, + k8sGetLogsTool, + k8sApplyManifestTool, + k8sDeleteResourceTool, + k8sListServicesTool, + k8sListDeploymentsTool, + k8sGetConfigMapTool, + k8sSwitchContextTool, + k8sGetNamespacesTool, + k8sWaitForDeploymentTool, +}; + +export const k8sToolsProject: ToolProject = { + manifest: { + key: 'k8s', + name: 'k8s-tools', + displayName: 'Kubernetes', + version: '1.0.0', + description: 'Kubernetes command and cluster inspection tools for working with kubectl and manifests.', + author: 'toolpack-sdk', + tools: [ + 'k8s.list_pods', + 'k8s.describe', + 'k8s.get_logs', + 'k8s.apply_manifest', + 'k8s.delete_resource', + 'k8s.list_services', + 'k8s.list_deployments', + 'k8s.get_config_map', + 'k8s.switch_context', + 'k8s.get_namespaces', + 'k8s.wait_for_deployment', + ], + category: 'kubernetes', + }, + tools: [ + k8sListPodsTool, + k8sDescribeTool, + k8sGetLogsTool, + k8sApplyManifestTool, + k8sDeleteResourceTool, + k8sListServicesTool, + k8sListDeploymentsTool, + k8sGetConfigMapTool, + k8sSwitchContextTool, + k8sGetNamespacesTool, + k8sWaitForDeploymentTool, + ], +}; diff --git a/packages/toolpack-sdk/src/tools/k8s-tools/tools.ts b/packages/toolpack-sdk/src/tools/k8s-tools/tools.ts new file mode 100644 index 0000000..f78b2cb --- /dev/null +++ b/packages/toolpack-sdk/src/tools/k8s-tools/tools.ts @@ -0,0 +1,374 @@ +import { execFileSync } from 'child_process'; +import { ToolDefinition } from '../types.js'; +import { logDebug } from '../../providers/provider-logger.js'; + +function ensureSafeKubectlArg(value: string, name: string): string { + if (value.includes('\0') || value.includes('\n') || value.includes('\r')) { + throw new Error(`Invalid ${name}: contains disallowed characters.`); + } + return value; +} + +function formatKubectlError(error: any): string { + const stdout = typeof error.stdout === 'string' ? error.stdout : ''; + const stderr = typeof error.stderr === 'string' ? error.stderr : ''; + const status = error.status ?? 'unknown'; + const stderrLines = stderr.split(/\r?\n/).filter((line: string) => line.trim().length > 0); + const kubectlMessage = stderrLines.find((line: string) => line.toLowerCase().startsWith('error:')) || stderrLines[0] || error.message || ''; + return `kubectl failed (exit code ${status})${kubectlMessage ? `: ${kubectlMessage.trim()}` : ''}\nSTDOUT:\n${stdout}\nSTDERR:\n${stderr}`; +} + +function runKubectl(args: string[], stdin?: string, timeoutMs = 30_000): string { + args.forEach((arg, index) => ensureSafeKubectlArg(arg, `kubectl argument #${index}`)); + logDebug(`[k8s-tools] execute: kubectl ${args.join(' ')}`); + + try { + const output = execFileSync('kubectl', args, { + input: stdin, + encoding: 'utf-8', + maxBuffer: 10 * 1024 * 1024, + stdio: ['pipe', 'pipe', 'pipe'], + timeout: timeoutMs, + }); + return output || '(kubectl completed with no output)'; + } catch (error: any) { + return formatKubectlError(error); + } +} + +function parseKubectlTimeout(timeout?: string): number { + if (!timeout) return 300_000; + const normalized = timeout.trim().toLowerCase(); + const match = /^([0-9]+)(s|m|h)?$/.exec(normalized); + if (!match) return 300_000; + + const value = Number(match[1]); + switch (match[2]) { + case 'h': + return value * 60 * 60 * 1000; + case 'm': + return value * 60 * 1000; + default: + return value * 1000; + } +} + +const category = 'kubernetes'; + +function toLabelSelector(labelInput: string | Record | undefined): string | undefined { + if (!labelInput) return undefined; + if (typeof labelInput === 'string') return labelInput; + const entries = Object.entries(labelInput).filter(([, value]) => value !== undefined && value !== ''); + if (!entries.length) return undefined; + return entries.map(([key, value]) => `${ensureSafeKubectlArg(key, 'label key')}=${ensureSafeKubectlArg(value, 'label value')}`).join(','); +} + +export const k8sListPodsTool: ToolDefinition = { + name: 'k8s.list_pods', + displayName: 'Kubernetes List Pods', + description: 'List pods in the current or a specific Kubernetes namespace.', + category, + parameters: { + type: 'object', + properties: { + namespace: { type: 'string', description: 'Namespace to query. If omitted, uses the current namespace.' }, + labels: { type: 'string', description: 'Label selector to filter pods.', }, + labelSelector: { + type: 'object', + description: 'Map of label key/value pairs to filter pods.', + additionalProperties: { type: 'string' }, + }, + output: { type: 'string', description: 'Output format for the pod list.', enum: ['wide', 'name', 'json', 'yaml'], default: 'wide' }, + allNamespaces: { type: 'boolean', description: 'If true, list pods across all namespaces.', default: false }, + }, + required: [], + }, + execute: async (args: Record) => { + const command = ['get', 'pods']; + const output = args.output as string | undefined; + + if (output) { + command.push('-o', output); + } else { + command.push('-o', 'wide'); + } + + if (args.allNamespaces) command.push('--all-namespaces'); + if (args.namespace && !args.allNamespaces) command.push('-n', ensureSafeKubectlArg(args.namespace as string, 'namespace')); + + const labelSelector = toLabelSelector(args.labelSelector as Record | string | undefined) ?? args.labels; + if (labelSelector) command.push('-l', ensureSafeKubectlArg(labelSelector, 'labels')); + + return runKubectl(command); + }, +}; + +export const k8sDescribeTool: ToolDefinition = { + name: 'k8s.describe', + displayName: 'Kubernetes Describe Resource', + description: 'Describe a Kubernetes resource or resource instance.', + category, + parameters: { + type: 'object', + properties: { + resource: { type: 'string', description: 'Resource type to describe, such as pod, service, deployment.', }, + name: { type: 'string', description: 'Resource name. Optional for cluster-wide descriptions.', }, + namespace: { type: 'string', description: 'Namespace containing the resource.', }, + }, + required: ['resource'], + }, + execute: async (args: Record) => { + const resource = ensureSafeKubectlArg(args.resource as string, 'resource'); + const command = ['describe', resource]; + if (args.name) command.push(ensureSafeKubectlArg(args.name as string, 'name')); + if (args.namespace) command.push('-n', ensureSafeKubectlArg(args.namespace as string, 'namespace')); + return runKubectl(command); + }, +}; + +export const k8sGetLogsTool: ToolDefinition = { + name: 'k8s.get_logs', + displayName: 'Kubernetes Get Pod Logs', + description: 'Fetch logs from a Kubernetes pod, optionally from a specific container.', + category, + parameters: { + type: 'object', + properties: { + podName: { type: 'string', description: 'The name of the pod to fetch logs from.' }, + namespace: { type: 'string', description: 'Namespace of the pod.', }, + container: { type: 'string', description: 'Container name inside the pod.', }, + tailLines: { type: 'number', description: 'Number of log lines to show from the end.', default: 100 }, + since: { type: 'string', description: 'Return logs newer than a relative duration like 5m or 1h.', }, + }, + required: ['podName'], + }, + execute: async (args: Record) => { + const command = ['logs', ensureSafeKubectlArg(args.podName as string, 'podName')]; + if (args.container) command.push('-c', ensureSafeKubectlArg(args.container as string, 'container')); + if (args.namespace) command.push('-n', ensureSafeKubectlArg(args.namespace as string, 'namespace')); + if (typeof args.tailLines === 'number') command.push('--tail', `${args.tailLines}`); + if (args.since) command.push('--since', ensureSafeKubectlArg(args.since as string, 'since')); + return runKubectl(command); + }, +}; + +export const k8sApplyManifestTool: ToolDefinition = { + name: 'k8s.apply_manifest', + displayName: 'Kubernetes Apply Manifest', + description: 'Apply a Kubernetes manifest from a file path or inline YAML content.', + category, + parameters: { + type: 'object', + properties: { + path: { type: 'string', description: 'Path to the manifest file to apply.', }, + manifest: { type: 'string', description: 'Inline YAML manifest to apply if no path is provided.', }, + namespace: { type: 'string', description: 'Namespace to apply the manifest into, if supported by the manifest.', }, + dryRun: { type: 'boolean', description: 'If true, perform a client-side dry run without applying changes.', default: false }, + }, + required: [], + }, + confirmation: { + level: 'high', + reason: 'This will change cluster state by applying a Kubernetes manifest.', + showArgs: ['path', 'namespace'], + }, + execute: async (args: Record) => { + const path = args.path as string | undefined; + const manifest = args.manifest as string | undefined; + const command = ['apply']; + if (args.dryRun) command.push('--dry-run=client'); + command.push('-f'); + if (path) { + command.push(ensureSafeKubectlArg(path, 'path')); + if (args.namespace) command.push('-n', ensureSafeKubectlArg(args.namespace as string, 'namespace')); + return runKubectl(command); + } + + if (!manifest) { + throw new Error('Either path or manifest is required to apply a Kubernetes manifest.'); + } + + if (args.namespace) command.push('-n', ensureSafeKubectlArg(args.namespace as string, 'namespace')); + command.push('-'); + return runKubectl(command, manifest); + }, +}; + +export const k8sDeleteResourceTool: ToolDefinition = { + name: 'k8s.delete_resource', + displayName: 'Kubernetes Delete Resource', + description: 'Delete a Kubernetes resource by type and name, or delete resources from a manifest file.', + category, + parameters: { + type: 'object', + properties: { + resource: { type: 'string', description: 'Resource type to delete, such as pod, service, deployment.', }, + name: { type: 'string', description: 'Name of the resource to delete.', }, + namespace: { type: 'string', description: 'Namespace containing the resource.', }, + path: { type: 'string', description: 'Path to a manifest file that contains the resources to delete.', }, + force: { type: 'boolean', description: 'Force deletion of the resource.', default: false }, + dryRun: { type: 'boolean', description: 'If true, perform a client-side dry run without deleting resources.', default: false }, + }, + required: [], + }, + confirmation: { + level: 'high', + reason: 'This will delete resources from the Kubernetes cluster.', + showArgs: ['resource', 'name', 'path'], + }, + execute: async (args: Record) => { + const path = args.path as string | undefined; + const resource = args.resource as string | undefined; + const name = args.name as string | undefined; + + if (path) { + const command = ['delete']; + if (args.dryRun) command.push('--dry-run=client'); + command.push('-f', ensureSafeKubectlArg(path, 'path')); + if (args.namespace) command.push('-n', ensureSafeKubectlArg(args.namespace as string, 'namespace')); + return runKubectl(command); + } + + if (!resource || !name) { + throw new Error('resource and name are required unless a manifest path is provided.'); + } + + const command = ['delete', ensureSafeKubectlArg(resource, 'resource'), ensureSafeKubectlArg(name, 'name')]; + if (args.namespace) command.push('-n', ensureSafeKubectlArg(args.namespace as string, 'namespace')); + if (args.force) command.push('--force', '--grace-period=0'); + if (args.dryRun) command.push('--dry-run=client'); + return runKubectl(command); + }, +}; + +export const k8sListServicesTool: ToolDefinition = { + name: 'k8s.list_services', + displayName: 'Kubernetes List Services', + description: 'List services in the current or a specific Kubernetes namespace.', + category, + parameters: { + type: 'object', + properties: { + namespace: { type: 'string', description: 'Namespace to query. If omitted, uses the current namespace.' }, + output: { type: 'string', description: 'Output format for the service list.', enum: ['wide', 'name', 'json', 'yaml'], default: 'wide' }, + allNamespaces: { type: 'boolean', description: 'List services across all namespaces.', default: false }, + }, + required: [], + }, + execute: async (args: Record) => { + const command = ['get', 'services']; + const output = args.output as string | undefined; + command.push('-o', output || 'wide'); + if (args.allNamespaces) command.push('--all-namespaces'); + if (args.namespace && !args.allNamespaces) command.push('-n', ensureSafeKubectlArg(args.namespace as string, 'namespace')); + return runKubectl(command); + }, +}; + +export const k8sListDeploymentsTool: ToolDefinition = { + name: 'k8s.list_deployments', + displayName: 'Kubernetes List Deployments', + description: 'List deployments in the current or a specific Kubernetes namespace.', + category, + parameters: { + type: 'object', + properties: { + namespace: { type: 'string', description: 'Namespace to query. If omitted, uses the current namespace.' }, + labels: { type: 'string', description: 'Label selector to filter deployments.', }, + labelSelector: { + type: 'object', + description: 'Map of label key/value pairs to filter deployments.', + additionalProperties: { type: 'string' }, + }, + output: { type: 'string', description: 'Output format for the deployment list.', enum: ['wide', 'name', 'json', 'yaml'], default: 'wide' }, + allNamespaces: { type: 'boolean', description: 'List deployments across all namespaces.', default: false }, + }, + required: [], + }, + execute: async (args: Record) => { + const command = ['get', 'deployments']; + const output = args.output as string | undefined; + command.push('-o', output || 'wide'); + if (args.allNamespaces) command.push('--all-namespaces'); + if (args.namespace && !args.allNamespaces) command.push('-n', ensureSafeKubectlArg(args.namespace as string, 'namespace')); + + const labelSelector = toLabelSelector(args.labelSelector as Record | string | undefined) ?? args.labels; + if (labelSelector) command.push('-l', ensureSafeKubectlArg(labelSelector, 'labels')); + return runKubectl(command); + }, +}; + +export const k8sGetConfigMapTool: ToolDefinition = { + name: 'k8s.get_config_map', + displayName: 'Kubernetes Get ConfigMap', + description: 'Retrieve a ConfigMap from a Kubernetes namespace.', + category, + parameters: { + type: 'object', + properties: { + name: { type: 'string', description: 'ConfigMap name.', }, + namespace: { type: 'string', description: 'Namespace containing the ConfigMap.', }, + output: { type: 'string', description: 'Output format, such as yaml or json.', enum: ['yaml', 'json'], default: 'yaml' }, + }, + required: ['name'], + }, + execute: async (args: Record) => { + const command = ['get', 'configmap', ensureSafeKubectlArg(args.name as string, 'name'), '-o', args.output as string || 'yaml']; + if (args.namespace) command.push('-n', ensureSafeKubectlArg(args.namespace as string, 'namespace')); + return runKubectl(command); + }, +}; + +export const k8sSwitchContextTool: ToolDefinition = { + name: 'k8s.switch_context', + displayName: 'Kubernetes Switch Context', + description: 'Switch the active kubectl context to a different Kubernetes cluster or namespace configuration.', + category, + parameters: { + type: 'object', + properties: { + context: { type: 'string', description: 'The kubeconfig context to switch to.', }, + }, + required: ['context'], + }, + execute: async (args: Record) => { + return runKubectl(['config', 'use-context', ensureSafeKubectlArg(args.context as string, 'context')]); + }, +}; + +export const k8sGetNamespacesTool: ToolDefinition = { + name: 'k8s.get_namespaces', + displayName: 'Kubernetes Get Namespaces', + description: 'List namespaces in the current Kubernetes context.', + category, + parameters: { + type: 'object', + properties: { + output: { type: 'string', description: 'Output format for the namespace list.', enum: ['wide', 'name', 'json', 'yaml'], default: 'wide' }, + }, + required: [], + }, + execute: async (args: Record) => runKubectl(['get', 'namespaces', '-o', (args.output as string) || 'wide']), +}; + +export const k8sWaitForDeploymentTool: ToolDefinition = { + name: 'k8s.wait_for_deployment', + displayName: 'Kubernetes Wait For Deployment', + description: 'Wait for a Kubernetes deployment to complete its rollout.', + category, + parameters: { + type: 'object', + properties: { + name: { type: 'string', description: 'Deployment name to wait for.', }, + namespace: { type: 'string', description: 'Namespace containing the deployment.', }, + timeout: { type: 'string', description: 'Timeout duration, e.g. 300s or 5m.', default: '300s' }, + }, + required: ['name'], + }, + execute: async (args: Record) => { + const timeout = args.timeout as string | undefined; + const command = ['rollout', 'status', `deployment/${ensureSafeKubectlArg(args.name as string, 'name')}`, '--timeout', timeout || '300s']; + if (args.namespace) command.push('-n', ensureSafeKubectlArg(args.namespace as string, 'namespace')); + return runKubectl(command, undefined, parseKubectlTimeout(timeout)); + }, +}; diff --git a/packages/toolpack-sdk/src/tools/types.ts b/packages/toolpack-sdk/src/tools/types.ts index 0936a5a..7839aa6 100644 --- a/packages/toolpack-sdk/src/tools/types.ts +++ b/packages/toolpack-sdk/src/tools/types.ts @@ -11,6 +11,7 @@ export interface ToolParameterProperty { default?: any; items?: ToolParameterProperty; properties?: Record; + additionalProperties?: ToolParameterProperty | boolean; required?: string[]; }